Skip to content

Instantly share code, notes, and snippets.

@MrOrz
Created November 21, 2015 04:06
Show Gist options
  • Save MrOrz/50bda5e23db2c39713e1 to your computer and use it in GitHub Desktop.
Save MrOrz/50bda5e23db2c39713e1 to your computer and use it in GitHub Desktop.
mockStore would emit "done() called multiple times" error.

Steps to reproduce the bug

  1. Download the gist
  2. npm install
  3. npm test

This would lead you to the following output:

> [email protected] test /Users/mrorz/workspace/testAsyncReduxAction
> mocha --compilers js:babel-core/register test.js



  async actions
    1) shall not pass
    2) shall not pass


  0 passing (47ms)
  2 failing

  1) async actions shall not pass:
     Error: done() called multiple times
      at Suite.<anonymous> (test.js:57:3)
      at Object.<anonymous> (test.js:56:1)
      at loader (node_modules/babel-core/node_modules/babel-register/lib/node.js:127:5)
      at Object.require.extensions.(anonymous function) [as .js] (node_modules/babel-core/node_modules/babel-register/lib/node.js:137:7)
      at Array.forEach (native)
      at node.js:961:3

  2) async actions shall not pass:
     Error: done() called multiple times
      at Suite.<anonymous> (test.js:57:3)
      at Object.<anonymous> (test.js:56:1)
      at loader (node_modules/babel-core/node_modules/babel-register/lib/node.js:127:5)
      at Object.require.extensions.(anonymous function) [as .js] (node_modules/babel-core/node_modules/babel-register/lib/node.js:137:7)
      at Array.forEach (native)
      at node.js:961:3

Analysis

In test.js, the first "expectedAction" triggers an assertion error. The error is caught by the catch clause inside mockStore and done(e) is invoked. However, actionUnderTest still invokes dispatch the second time, causing done to be invoked again.

Proposed Fix

dispatch should be no-op after done() is invoked.

{
"name": "test-async-redux-action",
"version": "1.0.0",
"description": "",
"main": "test.js",
"scripts": {
"test": "mocha --compilers js:babel-core/register test.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"babel": "^6.1.18",
"babel-core": "^6.2.1",
"babel-preset-es2015": "^6.1.18",
"chai": "^3.4.1",
"mocha": "^2.3.4",
"redux": "^3.0.4",
"redux-thunk": "^1.0.0"
},
"babel": {
"presets": [
"es2015"
]
}
}
import {expect} from 'chai'
import { applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const middlewares = [ thunk ]
function actionUnderTest() {
return function(dispatch){
setTimeout(function(){dispatch({type: 'FOO'})}, 10);
setTimeout(function(){dispatch({type: 'BAR'})}, 20);
}
}
/**
* Creates a mock of Redux store with middleware.
*/
function mockStore(getState, expectedActions, done) {
if (!Array.isArray(expectedActions)) {
throw new Error('expectedActions should be an array of expected actions.')
}
if (typeof done !== 'undefined' && typeof done !== 'function') {
throw new Error('done should either be undefined or function.')
}
function mockStoreWithoutMiddleware() {
return {
getState() {
return typeof getState === 'function' ?
getState() :
getState
},
dispatch(action) {
const expectedAction = expectedActions.shift()
try {
expect(action).to.eql(expectedAction)
if (done && !expectedActions.length) {
done()
}
return action
} catch (e) {
done(e)
}
}
}
}
const mockStoreWithMiddleware = applyMiddleware(
...middlewares
)(mockStoreWithoutMiddleware)
return mockStoreWithMiddleware()
}
describe('async actions', () => {
it('shall not pass', (done) => {
const expectedActions = [
{ type: 'FOOOOOO' },
{ type: 'BAR' }
]
const store = mockStore({}, expectedActions, done)
store.dispatch(actionUnderTest());
})
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment