项目作者: eidoo

项目描述 :
Test helper for redux-saga
高级语言: JavaScript
项目地址: git://github.com/eidoo/redux-saga-mock.git
创建时间: 2016-11-28T10:05:40Z
项目社区:https://github.com/eidoo/redux-saga-mock

开源协议:MIT License

下载


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

  1. $ 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:

  1. function * mysaga() {
  2. try {
  3. const responseObj = yield call(window.fetch, 'https://some.host/some/path', { method: 'get' })
  4. const jsonData = yield responseObj.json()
  5. if (jsonData.someField) {
  6. yield put({ type: 'someAction', data: jsonData.someField })
  7. yield call(someFunction, jsonData.someField)
  8. }
  9. } catch (err) {
  10. yield put({ type: 'someError', error: err })
  11. }
  12. }

A simple test that checks the call to someFunction and the dispatch of the action someAction:

  1. import { runSaga } from 'redux-saga'
  2. import { mockSaga } from 'redux-saga-mock'
  3. import saga from './mysaga'
  4. const MOCK_RESPONSE = {
  5. json: () => Promise.resolve({ field: 'some data' })
  6. }
  7. it('sample unit test', () => {
  8. const testSaga = mockSaga(saga)
  9. testSaga.stubCall(window.fetch, () => Promise.resolve(MOCK_RESPONSE))
  10. testSaga.stubCall(someFunction, () => {})
  11. return runSaga(testSaga(), {}).done
  12. .then(() => {
  13. const query = testSaga.query()
  14. assert.isTrue(query.callWithArgs(someFunction, 'some data').isPresent)
  15. assert.isTrue(query.putAction({ type: 'someAction', data: 'some data' }).isPresent)
  16. })
  17. })

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.

  1. import { createStore, applyMiddleware } from 'redux'
  2. import createSagaMiddleware from 'redux-saga'
  3. import { mockSaga } from 'redux-saga-mock'
  4. import saga from './mysaga'
  5. const reducer = s => s
  6. const initialState = {}
  7. const MOCK_RESPONSE = {
  8. json: () => Promise.resolve({ field: 'some data' })
  9. }
  10. it('sample test', () => {
  11. const sagaMiddleware = createSagaMiddleware()
  12. const store = createStore(reducer, initialState, applyMiddleware(sagaMiddleware))
  13. const testSaga = mockSaga(saga)
  14. testSaga.stubCall(window.fetch, () => Promise.resolve(MOCK_RESPONSE))
  15. return sagaMiddleware.run(testSaga).done
  16. .then(() => {
  17. const query = testSaga.query()
  18. assert.isTrue(query.callWithArgs(someFunction, 'some data').isPresent)
  19. assert.isTrue(query.putAction({ type: 'someAction', data: 'some data' }).isPresent)
  20. })
  21. })

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.

  1. import { runSaga } from 'redux-saga'
  2. import { mockSaga } from 'redux-saga-mock'
  3. import saga from './mysaga'
  4. const MOCK_RESPONSE = {
  5. json: () => Promise.resolve({ field: 'some data' })
  6. }
  7. it('sample unit test', () => {
  8. const testSaga = mockSaga(saga)
  9. testSaga.stubCall(window.fetch, () => Promise.resolve(MOCK_RESPONSE))
  10. return runSaga(testSaga(), {}).done
  11. .then(() => {
  12. const query = testSaga.query()
  13. assert.isTrue(query.callWithArgs(someFunction, 'some data').isPresent)
  14. assert.isTrue(query.putAction({ type: 'someAction', data: 'some data' }).isPresent)
  15. })
  16. })

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)