redux-saga
framework provides a bunch of interesting patterns for working with side effects, but as true bloody-enterprise developers, we have to cover all our code with tests. Let's see how we test our sagas.
redux-saga
. I will give here the code of the whole saga file:
export function* processClick() { const result = yield call(ServerApi.SendClick) yield put(Actions.clickSuccess(result)) } export function* watchClick() { yield takeEvery(ActionTypes.CLICK, processClick) }
processClick
saga, which directly processes the action and the watchClick
saga, which creates a cycle of action processing.
( call)
, receives the result and sends it to the reducer ( put)
. We need to somehow test whether the saga sends exactly what it receives from the server. Let's get started
next()
method, which is in the prototype generator. When using this method, we have the ability to both get the next value from the generator, and transfer the value to the generator. In this way, we get a chance to get out of the box. But is everything so rosy? Here is a test that I wrote on bare generators:
it('should increment click counter (behaviour test)', () => { const saga = processClick() expect(saga.next().value).toEqual(call(ServerApi.SendClick)) expect(saga.next(10).value).toEqual(put(Actions.clickSuccess(10))) })
Such a test does not help in the development.
redux-saga
developers offer a look at several libraries created specifically to satisfy testing fans.
redux-saga-test-plan
. Here is the code for the first version of the test that I wrote with its help:
it('should increment click counter (behaviour test with test-plan)', () => { return expectSaga(processClick) .provide([ call(ServerApi.SendClick), 2] ]) .dispatch(Actions.click()) .call(ServerApi.SendClick) .put(Actions.clickSuccess(2)) .run() })
redux-saga-test-plan
is the expectSaga
function, which returns an interface that describes the test. The tested saga is passed to the function itself ( processClick
from the first listing).
provide
method, you can block server calls or other dependencies. An array from StaticProvider'
transferred to it, which describe which method should return.
Act
block, we have one single method - dispatch
. The action is passed to it, to which the saga will react.
assert
block consists of the call put
methods, which check whether the corresponding effects were caused during the saga.
run()
method. This method starts the test directly.
Advantages of this approach:
- checks if the method was called, not the sequence of calls;
- moki clearly describe what function is mocking and what is returned.
However, there is work to do:
- the code has become more;
- test difficult to read;
- This is a test of behavior, which means it is still associated with the implementation of the saga.
test-plan
allows us to specify an initial state
and pass a reducer
that must respond to the effects of put
generated by the saga. It looks like this:
it('should increment click counter (state test with test-plan)', () => { const initialState = { clickCount: 11, return expectSaga(processClick) .provide([ call(ServerApi.SendClick), 14] ]) .withReducer(rootReducer, initialState) .dispatch(Actions.click()) .run() .then(result => expect(result.storeState.clickCount).toBe(14)) })
then()
with async/await
:
it('should increment click counter (state test with test-plan async-way)', async () => { const initialState = { clickCount: 11, } const saga = expectSaga(processClick) .provide([ call(ServerApi.SendClick), 14] ]) .withReducer(rootReducer, initialState) const result = await saga.dispatch(Actions.click()).run() expect(result.storeState.clickCount).toBe(14) })
export function* processClick() { const result = yield call(ServerApi.SendClick) yield put(Actions.clickSuccess(result)) } export function* processUnclick() { const result = yield call(ServerApi.SendUnclick) yield put(Actions.clickSuccess(result)) } function* watchClick() { yield takeEvery(ActionTypes.CLICK, processClick) } function* watchUnclick() { yield takeEvery(ActionTypes.UNCLICK, processUnclick) } export default function* mainSaga() { yield all([watchClick(), watchUnclick()]) }
redux-saga-test-plan
:
it('should change click counter (integration test)', async () => { const initialState = { clickCount: 11, } const saga = expectSaga(mainSaga) .provide([ call(ServerApi.SendClick), 14], call(ServerApi.SendUnclick), 18] ]) .withReducer(rootReducer, initialState) const result = await saga .dispatch(Actions.click()) .dispatch(Actions.unclick()) .run() expect(result.storeState.clickCount).toBe(18) })
mainSaga
, not individual mainSaga
handlers.
takeEvery
effect - this is a message loop that will work while our application is open. Accordingly, a test in which takeEvery
is called takeEvery
not complete work without assistance, and the redux-saga-test-plan
forcibly terminates such effects 250 ms after the start of the test. This timeout can be changed by calling expectSaga.DEFAULT_TIMEOUT = 50.
If you do not want to receive such versions, one for each test with a complex effect, simply use thesilentRun()
method instead of therun()
method.
redux-saga-test-plan
is able to work with it only on JS so far.
npm install redux-saga-test-plan@beta
redux-saga-test-plan
the best library for testing redux-saga
. If you have redux-saga
in the project, it may be a good choice for you.
Source: https://habr.com/ru/post/458500/