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

Cannot figure out how to test redux-saga function with redux-saga-test-plan

我正在自学如何使用 redux-saga,同时自学单元测试,特别是 Jest。我从 redux-saga 的文档中获取了一个示例 saga,在这里:

http://yelouafi.github.io/redux-saga/docs/advanced/NonBlockingCalls.html

...并出于我自己的目的对其进行了修改。它应该是一个简单的身份验证处理程序,它监听登录或注销操作(因为该函数不知道用户是否登录),然后采取适当的操作。我已经在应用程序中测试了该功能,它似乎按预期运行,这很好。这是函数:

function* authFlow() { 
    while (true) {
        const initialAction = yield take (['LOGIN', 'LOGOUT']);
        if (initialAction.type === 'LOGIN') {
            const { username, password } = initialAction.payload;
            const authTask = yield fork(
                authorizeWithRemoteServer, 
                { username: username, password: password }
            );
            const action = yield take(['LOGOUT', 'LOGIN_FAIL']);
            if (action.type === 'LOGOUT') {
                yield cancel(authTask);
                yield call (unauthorizeWithRemoteServer)
            }
        } else {
            yield call (unauthorizeWithRemoteServer)
        }
    }
}

这看起来相当简单,但我很难对其进行测试。以下是我基于 Jest 的测试脚本的注释版本:

it ('authFlow() should work with successful login and then successful logout', () => {
  const mockCredentials = { 
      username: 'User', 
      password: 'goodpassword' 
  };
  testSaga( stateAuth.watcher )
    // This should test the first 'yield', which is 
    // waiting for LOGIN or LOGOUT. It works
    .next()
    .take(['LOGIN', 'LOGOUT'])

    // This should test 'authorizeWithRemoteServer', 
    // and appears to do that properly
    .next({ 
        type: 'LOGIN', 
        payload: mockCredentials 
    })
    .fork( 
        stateAuth.authorizeWithRemoteServer, 
        mockCredentials)

    // This should reflect 'yield take' after the 'yield fork', 
    // and does so
    .next()
    .take(['LOGOUT', 'LOGIN_FAIL'])

    /* 
       This is where I don't understand what's happening. 
       What I would think I should do is something like this, 
       if I want to test the logout path:
       .next({ type: 'LOGOUT' })
       .cancel(createMockTask())

       ...but that results in the following, perhaps predictable, error:

       cancel(task): argument task is undefined

       What I found does make the test not fail is the following line, but 
       I do not understand why it works. The fact that it matches 
       "take(['LOGIN', 'LOGOUT'])" indicates that it has 
       looped back to the top of the generator
    */
    .next(createMockTask())
    .take(['LOGIN', 'LOGOUT'])
})

所以要么我做错了 sagas,要么我不明白如何测试 sagas,要么测试这种 saga 真的很难而且可能不切实际。

这是怎么回事?提前致谢!

不知道答案是否仍然与您相关,但以防万一其他人将来偶然发现此问题:

行中

.next().take(['LOGOUT', 'LOGIN_FAIL'])

你基本上是通过了undefined,这意味着这一行的产量:

const action = yield take(['LOGOUT', 'LOGIN_FAIL']);

导致 action 成为 undefined

您应该做的是在该行传递模拟任务:

.next(createMockTask()).take(['LOGOUT', 'LOGIN_FAIL'])

我认为这将是正确的测试

it ('authFlow() should work with successful login and then successful logout', () => {
  const mockCredentials = {username: 'User', password: 'goodpassword'};
  testSaga( stateAuth.watcher )
    //this should test the first 'yield', which is waiting for LOGIN or LOGOUT. It works
    .next().take(['LOGIN', 'LOGOUT'])

    // This should test 'authorizeWithRemoteServer', and appears to do that properly
    .next({type: 'LOGIN', payload: mockCredentials}).fork( stateAuth.authorizeWithRemoteServer, mockCredentials)

    // We pass a mock task here
    .next(createMockTask()).take(['LOGOUT', 'LOGIN_FAIL'])

    // And then this should be correct
    .next({type: 'LOGOUT'}).cancel(createMockTask())

    // after which the saga loops back
    .take(['LOGIN', 'LOGOUT'])
})

请记住,调用next()时,您正在完成之前的收益率。

更新:糟糕,应该存储 createMockTask() 的结果以便能够将其用于断言。这应该是正确的代码:

it ('authFlow() should work with successful login and then successful logout', () => {
  const mockCredentials = {username: 'User', password: 'goodpassword'};
  const mockTask = createMockTask();
  testSaga( stateAuth.watcher )
    //this should test the first 'yield', which is waiting for LOGIN or LOGOUT. It works
    .next().take(['LOGIN', 'LOGOUT'])

    // This should test 'authorizeWithRemoteServer', and appears to do that properly
    .next({type: 'LOGIN', payload: mockCredentials}).fork( stateAuth.authorizeWithRemoteServer, mockCredentials)

    // We pass a mock task here
    .next(mockTask).take(['LOGOUT', 'LOGIN_FAIL'])

    // And then this should be correct
    .next({type: 'LOGOUT'}).cancel(mockTask)

    // after which the saga loops back
    .take(['LOGIN', 'LOGOUT'])
})