redux-saga-mock
v1.2.0
Published
Testing helper for redux-saga
Downloads
5,938
Readme
redux-saga-mock
Testing helper for redux-saga.
Make effective unit and integration tests indipendent from the real implementation of sagas.
Creates a proxy over a redux saga that allow to listen for some effect and to make complex queries on all produced effects. It is also possible to "replace" function calls with mocked functions.
Getting Started
Installation
$ npm install --save-dev redux-saga-mock
Usage example
You create a "proxied saga" calling the mockSaga()
function on your saga. The returned saga is enhanced with with some
function useful for tests.
The saga to test:
function * mysaga() {
try {
const responseObj = yield call(window.fetch, 'https://some.host/some/path', { method: 'get' })
const jsonData = yield responseObj.json()
if (jsonData.someField) {
yield put({ type: 'someAction', data: jsonData.someField })
yield call(someFunction, jsonData.someField)
}
} catch (err) {
yield put({ type: 'someError', error: err })
}
}
A simple test that checks the call to someFunction
and the dispatch of the action someAction
:
import { runSaga } from 'redux-saga'
import { mockSaga } from 'redux-saga-mock'
import saga from './mysaga'
const MOCK_RESPONSE = {
json: () => Promise.resolve({ field: 'some data' })
}
it('sample unit test', () => {
const testSaga = mockSaga(saga)
testSaga.stubCall(window.fetch, () => Promise.resolve(MOCK_RESPONSE))
testSaga.stubCall(someFunction, () => {})
return runSaga(testSaga(), {}).done
.then(() => {
const query = testSaga.query()
assert.isTrue(query.callWithArgs(someFunction, 'some data').isPresent)
assert.isTrue(query.putAction({ type: 'someAction', data: 'some data' }).isPresent)
})
})
Documentation
Tests setup
You can test a saga with or without a real store and saga middleware. The second case is for simple unit tests.
Setup with a store and saga middleware
This is for integration tests or tests of complex sagas. You build a real store, eventually with a working reducer, and run the saga through the saga middleware.
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { mockSaga } from 'redux-saga-mock'
import saga from './mysaga'
const reducer = s => s
const initialState = {}
const MOCK_RESPONSE = {
json: () => Promise.resolve({ field: 'some data' })
}
it('sample test', () => {
const sagaMiddleware = createSagaMiddleware()
const store = createStore(reducer, initialState, applyMiddleware(sagaMiddleware))
const testSaga = mockSaga(saga)
testSaga.stubCall(window.fetch, () => Promise.resolve(MOCK_RESPONSE))
return sagaMiddleware.run(testSaga).done
.then(() => {
const query = testSaga.query()
assert.isTrue(query.callWithArgs(someFunction, 'some data').isPresent)
assert.isTrue(query.putAction({ type: 'someAction', data: 'some data' }).isPresent)
})
})
In the above test the call to the window.fetch
function is replaced by a stub function returning a Promise resolved with
the MOCK_RESPONSE object, simulating the behaviour of windows.fetch
.
The function someFunction
was not stubbed resulting in a effective call made by the saga middleware.
Setup without store
You can run a saga without a store with the runSaga()
function from redux-saga. This is for unit tests and you can use
it when you can mock all effects produced by the saga that need the store.
import { runSaga } from 'redux-saga'
import { mockSaga } from 'redux-saga-mock'
import saga from './mysaga'
const MOCK_RESPONSE = {
json: () => Promise.resolve({ field: 'some data' })
}
it('sample unit test', () => {
const testSaga = mockSaga(saga)
testSaga.stubCall(window.fetch, () => Promise.resolve(MOCK_RESPONSE))
return runSaga(testSaga(), {}).done
.then(() => {
const query = testSaga.query()
assert.isTrue(query.callWithArgs(someFunction, 'some data').isPresent)
assert.isTrue(query.putAction({ type: 'someAction', data: 'some data' }).isPresent)
})
})
Notice that runSaga
wants a generator object and not a generator function.
If you need to provide a state to resolve a select
effect you have to use the getState
field of the option object in
the runSaga()
call, see redux-saga documentation.
In the same way, if you need to dispatch an action to resolve the take
effects, you can use the subscribe
field,
but in this case is probably easier to use a real store.
Queries
The mockSaga()
call returns a "proxied saga" enhanced with a query()
function that allow to build complex queries
on produced effects. The query()
method returns an object representing the sequence of all produced effects, using its
methods you can filter this set to produce complex query.
Check if it was produced some effect:
saga.query().isPresent
Check if it was produced a take effect of some-action type:
saga.query().takeAction('some-action').isPresent
Check it it was produced a call effect followed by a take effect:
saga.query().call(someFunction).followedBy.takeAction('some-action').isPresent
Query object properties
- count: the number of effects
- effects: array of produced effects ordered by time
- isPresent: true if the set has some item
- notPresent: true if there are no effects
Query object methods
These methods filter the set of effects resulting from the query and are chainable using the followedBy
or precededBy
properties.
- effect(eff): filters all effects equal to
eff
, - putAction(action): filters all put effects matching the action parameter. If action is a string it indicates the action type and matches al puts of actions of this type. If action is an action object, only actions equal to action are matched.
- takeAction(pattern): filters all take effects equal to the
take(pattern)
call - call(fn): filter all call effects to the fn function, regardless function call parameters
- callWithArgs(fn, ...args): filter all call effects to the fn function with at least specified parameters
- callWithExactArgs(fn, ...args): filter all call effects to the fn function with exactly the specified parameters
- number(num): select the effect number num. Example:
saga.query().call(someFn).number(2).followedBy.call(otherFn).isPresent
true if otherFn() is called after two calls to someFn() - first(): select the first effect of the set.
Example:
saga.query().call(someFn).first().precededBy.putAction('SOME_ACTION').notPresent
true if there aren't puts of SOME_ACTION type actions before calling someFn() the first time - last(): select the last effect of the set
Replace function calls
You can mock a function call providing your function to be called, the returned value is returned to the saga in place of the original function result. To replace a call you can use one of the following methods of the proxied saga:
- stubCall(fn, stub): replace all call to fn, regardless of arguments, with a call to the stub function.
- stubCallWithArgs(fn, args, stub): replace all call to fn, with at least the arguments in the args array, with a call to the stub function.
- stubCallWithExactArgs(fn, args, stub): replace all call to fn, with exactly the arguments in the args array, with a call to the stub function.
Listening effects
If you want to be notified when an effect is produced you can use the following methods. These methods can be called providing or not providing a callback function, if the callback function is not provided a Promise is returned and it is resolved on first matching effect produced.
- onEffect(effect, callback): notify when a matching effect is produced
- onTakeAction(pattern, callback): notify all take effects equal to the
take(pattern)
call - onPutAction(action, callback): notify all put effects matching the action parameter. If action is a string it indicates the action type and matches al puts of actions of this type. If action is an action object, only actions equal to action are matched.
- onCall(fn, callback): notify all call effects to the fn function, regardless function call parameters
- onCallWithArgs(fn, args, callback): notify all call effects to the fn function with at least specified parameters
- onCallWithExactArgs(fn, args, callback): filter all call effects to the fn function with exactly the specified parameters
The callback function is called with the matched effect as parameter. When testing with a store and a redux saga middleware, the callback function (or the promises resolutions) is called before submitting the effect to the redux saga middleware.
For integration testing purpose there are equivalent methods called after the submission of the effect to the middleware,
when the result is available and before returning it to the original saga. In this case the argument of the callback is
an object with the fields effect
and result
:
onYieldEffect(effect, callback)
onYieldTakeAction(pattern, callback)
onYieldPutAction(action, callback)
onYieldCall(fn, callback)
onYieldCallWithArgs(fn, args, callback)
onYieldCallWithExactArgs(fn, args, callback)