如何用 jest 测试 redux saga?

Posted

技术标签:

【中文标题】如何用 jest 测试 redux saga?【英文标题】:How to test redux saga with jest? 【发布时间】:2018-06-10 12:27:26 【问题描述】:

react、react-redux/saga 和 jest 中的新功能

考虑:

-----组件()----

componentDidMount() 

    this.props.actions.initTodos(
        axios,
        ajaxURLConstants.WP_GET_TODOS,
        appStateActions.setAppInIdle,
        appStateActions.setAppInProcessing,
        todosActions.todosInitialized
    );


所以当我的 TodoApp 组件确实挂载时,它会调度 INIT_TODOS 操作,然后 我的根传奇正在侦听,并且当它捕获时它会产生相应的工人传奇以采取相应的行动。

-----对应的工人传奇-----

export function* initTodosSaga( action ) 

    try 

        yield put( action.setAppInProcessing() );

        let response = yield call( action.axios.get , action.WP_GET_TODOS );

        if ( response.data.status === "success" )
            yield put( action.todosInitialized( response.data.todos ) );
        else 

            console.log( response );
            alert( response.data.error_msg );

        

     catch ( error ) 

        console.log( "error" , error );
        alert( "Failed to load initial data" );            

    

    yield put( action.setAppInIdle() );


-----到目前为止的测试-----

import todos             from "../../__fixtures__/todos";
import  initTodosSaga  from "../todosSaga";

test( "saga test" , () => 

    let response = 
            status : "success",
            todos
        ,
        action = 
            axios : 
                get : function() 

                    return new Promise( ( resolve , reject ) => 

                        resolve( response );

                     );

                
            ,
            WP_GET_TODOS       : "dummy url",
            setAppInIdle       : jest.fn(),
            setAppInProcessing : jest.fn(),
            todosInitialized   : jest.fn()
        ;

    let initTodosSagaGen = initTodosSaga( action );

    initTodosSagaGen.next();

    expect( action.setAppInIdle ).toHaveBeenCalled();

 );

-----测试结果-----

所以重要的部分是这个

console.error node_modules\redux-saga\lib\internal\utils.js:240

在检查 put(action) 时未捕获:参数操作未定义

但我有 console.log 我在 worker saga 中通过测试的操作,实际上它不是未定义的

我错过了什么?

提前致谢。

---------更新------------

好的,请注意,它在这行代码上抱怨

yield put( action.setAppInIdle() );

在 try catch 块之外,所以我做了一些更改

1.) 我将上面的代码移到 try catch 块中,就在

的 else 语句之后
if ( response.data.status === "success" )

请检查上面的initTodosSaga代码

然后在我的传奇测试中,我测试

expect( action.setAppInProcessing ).toHaveBeenCalled();

而不是 setAppInIdle 间谍功能

这是测试结果

所以测试通过了! 但它仍然在抱怨操作未定义

现在有趣的是,如果在我的传奇测试中,如果我现在测试这个

expect( action.setAppInProcessing ).toHaveBeenCalled();
expect( action.setAppInIdle ).toHaveBeenCalled();

这是结果

所以现在它仍然抱怨动作仍未定义(我的屏幕截图中没有包含,但仍然与上面相同)

加上我关于 setAppInIdle 间谍函数的第二个断言没有被调用,但 setAppInProcessing 确实通过了!

我希望这些附加信息有助于解决这个问题。

【问题讨论】:

错误不在 initTodosSaga 中,它在 deleteTodoSaga 中,您能否编辑您的问题以包含该 saga 的代码和相应的测试,请? @PatrickHund 好的问题已编辑,我不确定为什么 deleteTodoSaga 在这里被开玩笑抱怨,因为我还没有为那个传奇创建测试。此外,deleteTodoSaga 还没有重构,我计划将所有内容传递给动作有效负载,以便轻松测试 saga,因为 saga 所需的几乎所有内容都已经在动作中(依赖注入),因此,我做了什么initTodosSaga. @PatrickHund 我已经编辑了问题,我只是替换了上面的图像截图。我已经注释掉了除了 initTodosSaga 之外的所有其他工人 sagas(及其用法)以避免混淆。屏幕截图现在显示它在 initTodosSaga 中抱怨。我重新运行了测试,确实它仍然抱怨动作未定义,现在在 initTodosSaga 好的,我现在就afk,希望以后可以看看 非常感谢,非常感谢,非常渴望学习。朋友节日快乐。 【参考方案1】:

如果没有外部库的帮助,似乎很难测试 redux saga

对我来说,我用过 https://github.com/jfairbank/redux-saga-test-plan

这个库很好。

这是我现在的测试

--------测试1------------

所以对于这个测试,我传递了动作负载,几乎所有 saga 运行所需的东西,例如。 axios ,动作创建函数等... 更像是遵循依赖注入的原理,所以很容易测试。

-----TodoApp 组件-----

componentDidMount() 

    this.props.actions.initTodos(
        axios,
        ajaxURLConstants.WP_GET_TODOS,
        appStateActions.setAppInIdle,
        appStateActions.setAppInProcessing,
        todosActions.todosInitialized,
        todosActions.todosFailedInit
    );


因此,当组件确实挂载时,它会触发我的根 saga 侦听并捕获的操作,然后生成相应的工作 saga 以采取相应的行动

再次注意,我传递了 worker saga 在操作负载上正常运行所需的所有必要数据。

--initTodoSaga(工人传奇)--

export function* initTodosSaga( action ) 

    try 

        yield put( action.setAppInProcessing() );

        let response = yield call( action.axios.get , action.WP_GET_TODOS );

        if ( response.data.status === "success" )
            yield put( action.todosInitialized( response.data.todos ) );
        else 

            console.log( response );
            alert( response.data.error_msg );

            yield put( action.todosFailedInit( response ) );

        

     catch ( error ) 

        console.log( "error" , error );
        alert( "Failed to load initial data" );

        yield put( action.todosFailedInit( error ) );

    

    yield put( action.setAppInIdle() );


-----传奇测试-----

import  expectSaga     from "redux-saga-test-plan";
import  initTodosSaga  from "../todosSaga";

test( "should initialize the ToDos state via the initTodoSaga" , () => 

    let response = 

            data : 
                status : "success",
                todos
            

        ,
        action = 
            axios : 
                get : function() 

                    return new Promise( ( resolve , reject ) => 

                        resolve( response );

                     );

                
            ,
            WP_GET_TODOS       : "dummy url",
            setAppInIdle       : appStateActions.setAppInIdle,
            setAppInProcessing : appStateActions.setAppInProcessing,
            todosInitialized   : todosStateActions.todosInitialized,
            todosFailedInit    : todosStateActions.todosFailedInit
        ;

    // This is the important bit
    // These are the assertions
    // Basically saying that the actions below inside the put should be dispatched when this saga is executed
    return expectSaga( initTodosSaga , action )
        .put( appStateActions.setAppInProcessing() )
        .put( todosStateActions.todosInitialized( todos ) )
        .put( appStateActions.setAppInIdle() )
        .run();

 );

我的测试通过了耶! :) 现在为了向您显示测试失败时的错误消息,我将在我的 initTodosSaga

中注释掉这行代码
yield put( action.setAppInIdle() );

所以现在断言

.put( appStateActions.setAppInIdle() )

现在应该失败

所以它输出 put expect unmet 这是有道理的,因为我们期望被解雇的行动没有

--------测试2--------强>

现在这个测试是针对一个 saga 的,它导入了一些它需要运行的东西,这与我在第一个测试中提供 axios、动作有效负载中的动作创建者不同

这个saga导入了axios,它需要操作的action creators

谢天谢地,Redux Saga 测试计划有一些辅助函数来“feed” 虚拟数据到 saga 中

我将跳过触发root saga正在侦听的动作的组件,这并不重要,我将直接粘贴saga和saga测试

----addTodoSaga----

/** global ajaxurl */
import axios                from "axios";
import  call , put        from "redux-saga/effects";
import * as appStateActions from "../actions/appStateActions";
import * as todosActions    from "../actions/todosActions";

export function* addTodoSaga( action ) 

    try 

        yield put( appStateActions.setAppInProcessing() );

        let formData = new FormData;

        formData.append( "todo" , JSON.stringify( action.todo ) );

        let response = yield call( axios.post , ajaxurl + "?action=wptd_add_todo" , formData );

        if ( response.data.status === "success" ) 

            yield put( todosActions.todoAdded( action.todo ) );
            action.successCallback();

         else 

            console.log( response );
            alert( response.data.error_msg );

        

     catch ( error ) 

        console.log( error );
        alert( "Failed to add new todo" );

    

    yield put( appStateActions.setAppInIdle() );


-----测试-----

import axios          from "axios";
import  expectSaga  from "redux-saga-test-plan";
import * as matchers  from "redux-saga-test-plan/matchers";
import * as appStateActions   from "../../actions/appStateActions";
import * as todosStateActions from "../../actions/todosActions";
import  addTodoSaga  from "../todosSaga";

test( "should dispatch TODO_ADDED action when adding new todo is successful" , () => 

   let response = 
            data :  status : "success" 
        ,
        todo = 
            id        : 1,
            completed : false,
            title     : "Browse 9gag tonight"
        ,
        action = 
            todo,
            successCallback : jest.fn()
        ;

    // Here are the assertions
    return expectSaga( addTodoSaga , action )
        .provide( [
            [ matchers.call.fn( axios.post ) , response ]
        ] )
        .put( appStateActions.setAppInProcessing() )
        .put( todosStateActions.todoAdded( todo ) )
        .put( appStateActions.setAppInIdle() )
        .run();

 );

所以提供函数允许你模拟一个函数调用,同时提供它应该返回的虚拟数据

就是这样,我现在可以测试我的传奇了!耶!

还有一件事,当我为我的 saga 运行测试时,会导致执行带有警报代码的代码

例如

alert( "Earth is not flat!" );

我在控制台上得到了这个

Error: Not implemented: window.alert

以及它下面的一堆堆栈跟踪,所以可能是因为节点上不存在警报对象?我该如何隐藏这个?如果你们有答案,请添加评论。

希望对大家有帮助

【讨论】:

我很高兴你明白了,我发布了一个没有 redux-saga-test-plan 的答案,我不知道。我今天学到了一些东西! ? 错误消息“错误:未实现:window.alert”是因为 Jest 测试未在浏览器中运行,而是使用 JSDom (github.com/tmpvar/jsdom) 与 Node.js 一起运行,这不会'没有警报功能 啊,我明白了,那没办法在日志上隐藏这个?无论如何感谢您的信息。 我不确定,使用我编写的版本(请参阅我的答案),我没有这个问题,但这也可能是因为我设置测试的方式. 你可以试试 window.alert = jest.fn();【参考方案2】:

这是您的测试的工作版本:

import todos from '../../__fixtures__/todos';
import  initTodosSaga  from '../todosSaga';
import  put, call  from 'redux-saga/effects';

test('saga test', () => 
    const response = 
        data: 
            status: 'success',
            todos
        
    ;
    const action = 
        axios: 
            get() 
        ,
        WP_GET_TODOS: 'dummy url',
        setAppInIdle: jest.fn().mockReturnValue( type: 'setAppInIdle' ),
        setAppInProcessing: jest.fn().mockReturnValue( type: 'setAppInProcessing' ),
        todosInitialized: jest.fn().mockReturnValue( type: 'todosInitialized' )
    ;
    let result;

    const initTodosSagaGen = initTodosSaga(action);

    result = initTodosSagaGen.next();
    expect(result.value).toEqual(put(action.setAppInProcessing()));
    expect(action.setAppInProcessing).toHaveBeenCalled();
    result = initTodosSagaGen.next();
    expect(result.value).toEqual(call(action.axios.get, action.WP_GET_TODOS));
    result = initTodosSagaGen.next(response);
    expect(action.todosInitialized).toHaveBeenCalled();
    expect(result.value).toEqual(put(action.todosInitialized(response.data.todos)));
    result = initTodosSagaGen.next();
    expect(action.setAppInIdle).toHaveBeenCalled();
    expect(result.value).toEqual(put(action.setAppInIdle()));
);

一些注意事项:

您实际上不必让模拟 Axios.get 返回任何内容 使用expect 语句,我将生成器的产量与我期望生成器执行的操作(即执行putcall 语句)进行比较 您的模拟响应中缺少 data 属性

【讨论】:

感谢这个人!稍后我会检查一下,不胜感激。 基本上复制saga并断言每个yield都等于它唯一可能的动作是不是有点傻?它基本上是始终成功的实现测试。

以上是关于如何用 jest 测试 redux saga?的主要内容,如果未能解决你的问题,请参考以下文章

无法弄清楚如何使用 redux-saga-test-plan 测试 redux-saga 功能

如何用 jest 测试 combineReducers

如何用 Jest 测试一系列数字?

如何用 jest 测试此 api 中间件是不是处理 fetch 引发的错误?

redux-saga 的单元测试抛出错误:必须在迭代器上调用 runSaga

如何使用 redux-saga-test-plan 测试选择器功能