2017-08-08 42 views
0

我的应用程序中的所有逻辑都存在于动作创建者(thunk)中。大多数动作创建者的逻辑并不复杂,它由条件表达式组成,条件表达式的条件是来自存储的值:如果存储中存在此值,则分派这些动作创建者,否则分派此动作。还有一些“聚合器”,它们是动作创造者,通常基于某些状态值的存在来派遣其他几个动作创建者;和api包装器,它们通过状态参数有条件地调用api抽象thunk,然后处理响应。使用条件逻辑和getState()使用测试复杂的redux thunk

要指出的是,大多数人使用getState函数来获取他们自己需要的所有东西,而不是将其作为参数接收。现在,这种方法给了我很好的帮助,并且非常简单,但是我对测试它很困难。直到现在,我按照这个建议写了所有的测试:https://github.com/reactjs/redux/issues/2179。基本上,在开始时,我使用其他一些操作设置所需状态,模拟获取调用,然后分配我打算测试的thunk,然后使用各种选择器检查状态。这在单个测试中同时测试多个动作,缩减器和选择器。我喜欢这个事实,我的测试完全验证了特定的用例,但我不确定这是否是一种好的做法。我的主要问题是有些thunk是无法测试的,因为他们派出了5个其他动作创建者,我很困惑如何至少验证他们被调用,除了检查状态是否改变,这反过来使Promise链变得庞大,并在多个测试中一遍又一遍地测试相同的功能。

我是新来的整个测试的东西,所有在互联网上的例子都是TODO列表或其他荒谬简单的CRUD应用程序,这没有帮助。您如何在复杂的应用程序中使用大量的条件逻辑,以及依赖多个状态节点的动作创建器来进行Reduce测试?

+0

不确定它是否可以帮助您处理所有用例,但您是否考虑过使用middleWares? –

回答

0

简短回答:模拟它。 =)

长答案:我个人更喜欢在测试中尽可能多地使用真实代码(即没有测试双打)。但有时它不值得,而你必须回头嘲笑。

,活像一个情况下,你描述的也有,你可能想/几件事情需要在你的测试,以检查:

  1. 这部分子的thunk从您的thunk下实际测试调度。
  2. 被测试的thunk正确地处理它发送的sub-thunk的结果。
  3. 被测试的thunk正确处理更新的状态,该状态由调度的子thunk更新。

根据您想要测试的上面哪种组合,您可能会使用不同的策略。例如,在被测试的thunk依赖于sub-thunk的结果的情况下,不需要检查sub-thunk是否被分派:只是模拟sub-thunk,以便返回特定的数据,这会影响特定测试中的thunk的行为,明智的方式(请参阅auth模拟以下摘录中的细节)。

让我们考虑下面的例子来说明可能的模拟策略。想象一下你必须实现授权功能。假设您的服务器通过http端点授权用户,并且在成功的情况下,将授权令牌发回,稍后用于打开websocket连接。假设您按照以下方式设计了该功能:

connect thunk,它将用户的登录名和密码分配给auth sub-thunk,然后通过http发送给定凭证。当服务器响应auth thunk store收到的令牌store(仅出于说明的原因)并予以解决。当auth解决,connect调度otherStuff thunk这将会,以及用令牌做一些其他的东西。最后connect通过wsApi打开套接字连接。

// ======= connect.js ======= 

import { auth, getToken } from './auth'; 
import * as wsApi from './ws'; 
import { otherStuff } from './other-stuff'; 

export const connect = (login, password) => (dispatch, getState) => { 
    // ... 

    return dispatch(auth(login, password)) 
    .then(() => { 
     const token = getToken(getState()); 
     dispatch(otherStuff(token)); 
     wsApi.connect(token); 
    }); 

    // ... 
}; 


// ======= auth.js ======= 

import * as httpApi from './http'; 

const saveToken = token => ({ type: 'auth/save-token', payload: token }); 

export const auth = (login, password) => 
    dispatch => 
    httpApi.login(login, password) 
    .then(token => dispatch(saveToken(token))); 

export const getToken = state => state.auth.token; 

export default (state = {}, action) => action.type === 'auth/save-token' ? { token: action.payload } : state; 


// ======= other-stuff.js ======= 

export const otherStuff = token => (dispatch) => { 
    // ... 
}; 

我们要做什么是嘲笑2层的thunk:authotherStuffconnect高度依赖于auth,因此我们将确保auth仅由检查connect行为调用,具体取决于我们传递给auth的模拟行为。 otherStuff的情况稍微复杂一些。没有办法检查它实际上是否派发,然后实现自定义中间件,这将记录所有调度的操作。总而言之测试将如下所示(我使用jest的嘲笑):

import { createStore, applyMiddleware, combineReducers } from 'redux'; 
import thunk from 'redux-thunk'; 
import { connect } from './connect'; 
import { auth, getToken } from './auth'; 
import { otherStuff } from './other-stuff'; 
import * as wsApi from './ws'; 
const authReducer = require.requireActual('./auth').default; 

jest.mock('./auth'); 
jest.mock('./ws'); 
jest.mock('./other-stuff'); 

const makeSpyMiddleware =() => { 
    const dispatch = jest.fn(); 
    return { 
    dispatch, 
    middleware: store => next => action => { 
     dispatch(action); 
     return next(action); 
    } 
    }; 
}; 

describe('connect',() => { 
    let store; 
    let spy; 

    beforeEach(() => { 
    jest.clearAllMocks(); 
    spy = makeSpyMiddleware(); 

    store = createStore(authReducer, {}, applyMiddleware(spy.middleware, thunk)); 

    auth.mockImplementation((login, password) =>() => { 
     if (login === 'user' && password == 'password') return Promise.resolve(); 
     return Promise.reject(); 
    }); 
    }); 

    test('happy path',() => { 
    getToken.mockImplementation(() => 'generated token'); 
    otherStuff.mockImplementation(token => ({ type: 'mocked/other-stuff', token })); 

    return store.dispatch(connect('user', 'password')).then(() => { 
     expect(wsApi.connect).toHaveBeenCalledWith('generated token'); 
     expect(spy.dispatch).toHaveBeenCalledWith({ type: 'mocked/other-stuff', token: 'generated token'}); 
    }); 
    }); 

    test('auth failed',() => { 
    return store.dispatch(connect('user', 'wrong-password')).catch(() => { 
     expect(wsApi.connect).not.toHaveBeenCalled(); 
    }); 
    }); 
}); 

如果您需要在给定的片段任何意见,随意问。