Skip to content

Commit d6375d0

Browse files
committed
Merge pull request #878 from rackt/use-apply-middleware-in-tests
Suggest to use applyMiddleware() for testing async action creators
2 parents ab3904b + 978d663 commit d6375d0

File tree

3 files changed

+111
-38
lines changed

3 files changed

+111
-38
lines changed

docs/recipes/WritingTests.md

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ describe('actions', () => {
6060
});
6161
```
6262

63-
If you write async action creators
63+
### Async Action Creators
64+
65+
For async action creators using [Redux Thunk](https://github.com/gaearon/redux-thunk) or other middleware, it’s best to completely mock the Redux store for tests. You can still use [`applyMiddleware()`](../api/applyMiddleware.md) with a mock store, as shown below. You can also use [nock](https://github.com/pgte/nock) to mock the HTTP requests.
6466

6567
```js
6668
function fetchTodosRequest() {
@@ -98,31 +100,67 @@ can be tested like:
98100

99101
```js
100102
import expect from 'expect';
101-
import * as actions from '../../actions/TodoActions';
103+
import { applyMiddleware } from 'redux';
104+
import thunk from 'redux-thunk';
105+
import * as actions from '../../actions/counter';
102106
import * as types from '../../constants/ActionTypes';
103107
import nock from 'nock';
104108

109+
const middlewares = [thunk];
110+
111+
/**
112+
* Creates a mock of Redux store with middleware.
113+
*/
114+
function mockStore(getState, expectedActions, onLastAction) {
115+
if (!Array.isArray(expectedActions)) {
116+
throw new Error('expectedActions should be an array of expected actions.');
117+
}
118+
if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') {
119+
throw new Error('onLastAction should either be undefined or function.');
120+
}
121+
122+
function mockStoreWithoutMiddleware() {
123+
return {
124+
getState() {
125+
return typeof getState === 'function' ?
126+
getState() :
127+
getState;
128+
},
129+
130+
dispatch(action) {
131+
const expectedAction = expectedActions.shift();
132+
expect(action).toEqual(expectedAction);
133+
if (onLastAction && !expectedActions.length) {
134+
onLastAction();
135+
}
136+
return action;
137+
}
138+
}
139+
}
140+
141+
const mockStoreWithMiddleware = applyMiddleware(
142+
...middlewares
143+
)(mockStoreWithoutMiddleware);
144+
145+
return mockStoreWithMiddleware();
146+
}
147+
105148
describe('async actions', () => {
106-
afterEach(() => nock.cleanAll() );
149+
afterEach(() => {
150+
nock.cleanAll();
151+
});
107152

108-
it('creates FETCH_TODO_SUCCESS when fechting todos has been done', (done) => {
153+
it('creates FETCH_TODO_SUCCESS when fetching todos has been done', (done) => {
109154
nock('http://example.com/')
110155
.get('/todos')
111156
.reply(200, { todos: ['do something'] });
112157

113-
let expectedActions = [
158+
const expectedActions = [
114159
{ type: types.FETCH_TODO_REQUEST },
115160
{ type: types.FETCH_TODO_SUCCESS, body: { todos: ['do something'] } }
116161
]
117-
118-
function mockDispatch(action) {
119-
var expectedAction = expectedActions.shift();
120-
expect(action).toEqual(expectedAction);
121-
if (!expectedActions.length) {
122-
done();
123-
}
124-
}
125-
actions.fetchTodos()(mockDispatch);
162+
const store = mockStore({ todos: [] }, expectedActions, done);
163+
store.dispatch(actions.fetchTodos());
126164
});
127165
});
128166
```
Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,47 @@
11
import expect from 'expect';
2+
import { applyMiddleware } from 'redux';
3+
import thunk from 'redux-thunk';
24
import * as actions from '../../actions/counter';
35

6+
const middlewares = [thunk];
7+
8+
/*
9+
* Creates a mock of Redux store with middleware.
10+
*/
11+
function mockStore(getState, expectedActions, onLastAction) {
12+
if (!Array.isArray(expectedActions)) {
13+
throw new Error('expectedActions should be an array of expected actions.');
14+
}
15+
if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') {
16+
throw new Error('onLastAction should either be undefined or function.');
17+
}
18+
19+
function mockStoreWithoutMiddleware() {
20+
return {
21+
getState() {
22+
return typeof getState === 'function' ?
23+
getState() :
24+
getState;
25+
},
26+
27+
dispatch(action) {
28+
const expectedAction = expectedActions.shift();
29+
expect(action).toEqual(expectedAction);
30+
if (onLastAction && !expectedActions.length) {
31+
onLastAction();
32+
}
33+
return action;
34+
}
35+
};
36+
}
37+
38+
const mockStoreWithMiddleware = applyMiddleware(
39+
...middlewares
40+
)(mockStoreWithoutMiddleware);
41+
42+
return mockStoreWithMiddleware();
43+
}
44+
445
describe('actions', () => {
546
it('increment should create increment action', () => {
647
expect(actions.increment()).toEqual({ type: actions.INCREMENT_COUNTER });
@@ -10,32 +51,26 @@ describe('actions', () => {
1051
expect(actions.decrement()).toEqual({ type: actions.DECREMENT_COUNTER });
1152
});
1253

13-
it('incrementIfOdd should create increment action', () => {
14-
const fn = actions.incrementIfOdd();
15-
expect(fn).toBeA('function');
16-
const dispatch = expect.createSpy();
17-
const getState = () => ({ counter: 1 });
18-
fn(dispatch, getState);
19-
expect(dispatch).toHaveBeenCalledWith({ type: actions.INCREMENT_COUNTER });
54+
it('incrementIfOdd should create increment action', (done) => {
55+
const expectedActions = [
56+
{ type: actions.INCREMENT_COUNTER }
57+
];
58+
const store = mockStore({ counter: 1 }, expectedActions, done);
59+
store.dispatch(actions.incrementIfOdd());
2060
});
2161

22-
it('incrementIfOdd shouldnt create increment action if counter is even', () => {
23-
const fn = actions.incrementIfOdd();
24-
const dispatch = expect.createSpy();
25-
const getState = () => ({ counter: 2 });
26-
fn(dispatch, getState);
27-
expect(dispatch.calls.length).toBe(0);
62+
it('incrementIfOdd shouldnt create increment action if counter is even', (done) => {
63+
const expectedActions = [];
64+
const store = mockStore({ counter: 2 }, expectedActions);
65+
store.dispatch(actions.incrementIfOdd());
66+
done();
2867
});
2968

30-
// There's no nice way to test this at the moment...
31-
it('incrementAsync', (done) => {
32-
const fn = actions.incrementAsync(1);
33-
expect(fn).toBeA('function');
34-
const dispatch = expect.createSpy();
35-
fn(dispatch);
36-
setTimeout(() => {
37-
expect(dispatch).toHaveBeenCalledWith({ type: actions.INCREMENT_COUNTER });
38-
done();
39-
}, 5);
69+
it('incrementAsync should create increment action', (done) => {
70+
const expectedActions = [
71+
{ type: actions.INCREMENT_COUNTER }
72+
];
73+
const store = mockStore({ counter: 0 }, expectedActions, done);
74+
store.dispatch(actions.incrementAsync(100));
4075
});
4176
});

examples/real-world/createDevToolsWindow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function createDevToolsWindow(store) {
1616
);
1717

1818
if (!win) {
19-
console.error(
19+
console.error( // eslint-disable-line no-console
2020
'Couldn\'t open Redux DevTools due to a popup blocker. ' +
2121
'Please disable the popup blocker for the current page.'
2222
);

0 commit comments

Comments
 (0)