Skip to content

Commit 988519f

Browse files
author
Antoine Jaussoin
committed
Merge branch 'release/v1.0.5'
2 parents 7d98cbe + 6f6363a commit 988519f

11 files changed

+263
-108
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ jspm_packages
3737
.node_repl_history
3838
.npmrc
3939
.yarnrc
40-
.vscode
40+
.vscode

.vscode/temp.sql

Whitespace-only changes.

README.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ This examples is now actually using the `redux-saga` utility functions.
102102

103103
The important point to note here, is that Sagas **describe** what happens, and don't actually act on it.
104104
For example, an API will never be called, you don't have to mock it, when using `call`.
105+
Same thing for a selector, you don't need to mock the state when using `yield select(mySelector)`.
105106

106107
This makes testing very easy indeed.
107108

@@ -146,25 +147,29 @@ describe('When testing a very simple Saga', () => {
146147

147148
## Testing a complex Saga
148149

149-
This example deals with pretty much all use-cases for using Sagas, which involves calling an API, getting exceptions, and have some conditional logic based on some inputs.
150+
This example deals with pretty much all use-cases for using Sagas, which involves using a `select`or, `call`ing an API, getting exceptions, have some conditional logic based on some inputs and `put`ing new actions.
150151

151152
```javascript
152153
import sagaHelper from 'redux-saga-testing';
153-
import { call, put } from 'redux-saga/effects';
154+
import { call, put, select } from 'redux-saga/effects';
154155

155156
const splitApi = jest.fn();
156157
const someActionSuccess = payload => ({ type: 'SOME_ACTION_SUCCESS', payload });
157158
const someActionEmpty = () => ({ type: 'SOME_ACTION_EMPTY' });
158159
const someActionError = error => ({ type: 'SOME_ACTION_ERROR', payload: error });
160+
const selectFilters = state => state.filters;
159161

160162
function* mySaga(input) {
161163
try {
164+
// We get the filters list from the state, using "select"
165+
const filters = yield select(selectFilters);
166+
162167
// We try to call the API, with the given input
163168
// We expect this API takes a string and returns an array of all the words, split by comma
164169
const someData = yield call(splitApi, input);
165170

166171
// From the data we get from the API, we filter out the words 'foo' and 'bar'
167-
const transformedData = someData.filter(w => ['foo', 'bar'].indexOf(w) === -1);
172+
const transformedData = someData.filter(w => filters.indexOf(w) === -1);
168173

169174
// If the resulting array is empty, we call the empty action, otherwise we call the success action
170175
if (transformedData.length === 0) {
@@ -184,6 +189,14 @@ describe('When testing a complex Saga', () => {
184189
describe('Scenario 1: When the input contains other words than foo and bar and doesn\'t throw', () => {
185190
const it = sagaHelper(mySaga('hello,foo,bar,world'));
186191

192+
it('should get the list of filters from the state', result => {
193+
expect(result).toEqual(select(selectFilters));
194+
195+
// Here we specify what the selector should have returned.
196+
// The selector is not called so we have to give its expected return value.
197+
return ['foo', 'bar'];
198+
});
199+
187200
it('should have called the mock API first, which we are going to specify the results of', result => {
188201
expect(result).toEqual(call(splitApi, 'hello,foo,bar,world'));
189202

@@ -204,6 +217,11 @@ describe('When testing a complex Saga', () => {
204217
describe('Scenario 2: When the input only contains foo and bar', () => {
205218
const it = sagaHelper(mySaga('foo,bar'));
206219

220+
it('should get the list of filters from the state', result => {
221+
expect(result).toEqual(select(selectFilters));
222+
return ['foo', 'bar'];
223+
});
224+
207225
it('should have called the mock API first, which we are going to specify the results of', result => {
208226
expect(result).toEqual(call(splitApi, 'foo,bar'));
209227
return ['foo', 'bar'];
@@ -221,6 +239,11 @@ describe('When testing a complex Saga', () => {
221239
describe('Scenario 3: The API is broken and throws an exception', () => {
222240
const it = sagaHelper(mySaga('hello,foo,bar,world'));
223241

242+
it('should get the list of filters from the state', result => {
243+
expect(result).toEqual(select(selectFilters));
244+
return ['foo', 'bar'];
245+
});
246+
224247
it('should have called the mock API first, which will throw an exception', result => {
225248
expect(result).toEqual(call(splitApi, 'hello,foo,bar,world'));
226249

@@ -249,7 +272,7 @@ You have other examples in the [various](https://github.com/antoinejaussoin/redu
249272

250273
## FAQ
251274

252-
- How can I test a Saga that uses `take` or `takeEvery`?
275+
#### How can I test a Saga that uses `take` or `takeEvery`?
253276

254277
You should separate this generator in two: one that only uses `take` or `takeEvery` (the "watchers"), and the ones that atually run the code when the wait is over, like so:
255278

@@ -278,6 +301,11 @@ export default function* rootSaga() {
278301

279302
From the previous example, you don't have to test `rootSaga` but you can test `onSomeAction` and `onAnotherAction`.
280303

304+
#### Do I need to mock the store and/or the state?
305+
306+
No you don't. If you read the examples above carefuly, you'll notice that the actual selector (for example) is never called. That means you don't need to mock anything, just return the value your selector should have returned.
307+
This library is to test a saga *workflow*, not about testing your actual *selectors*. If you need to test a selector, do it in isolation (it's just a pure function after all).
308+
281309

282310
## Code coverage
283311

@@ -287,6 +315,10 @@ In the GitHub repo, you'll find examples using **Istanbul** (for Mocha) and **Je
287315

288316
## Change Log
289317

318+
### v1.0.5
319+
- Updating dependencies
320+
- Adding examples using `selector`s (thanks [@TAGC](https://github.com/tagc) for the suggestion)
321+
290322
### v1.0.4
291323

292324
- Updating dependencies

ava/06.saga-using-selectors.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import sagaHelper from '../main';
2+
import { call, select, put } from 'redux-saga/effects';
3+
import testAva from 'ava';
4+
5+
const loginAction = payload => ({ type: 'LOGIN', payload });
6+
const selectUsername = state => state.foo;
7+
8+
function* loginSaga() {
9+
const username = yield select(selectUsername);
10+
yield put(loginAction(username));
11+
}
12+
13+
const test = sagaHelper(loginSaga(), testAva);
14+
15+
test('should select the current username from the state', (result, t) => {
16+
// Remember, we are not testing the selector here, just testing that the "select" function was called with a given selector function
17+
// The selector won't be executed (and it can't anyway, there is no state known to redux-saga on this test)
18+
t.deepEqual(result, select(selectUsername));
19+
// We return what the selector should have returned
20+
return 'donald.drumpf';
21+
});
22+
23+
test('should trigger the login action with the username we got from the state', (result, t) => {
24+
t.deepEqual(result, put(loginAction('donald.drumpf')));
25+
});
26+
27+
test('and then nothing', (result, t) => {
28+
t.is(result, undefined);
29+
});

ava/10.all-in-one-example.test.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import sagaHelper from '../main';
2-
import { call, put } from 'redux-saga/effects';
2+
import { call, put, select } from 'redux-saga/effects';
33
import testAva from 'ava';
44
import sinon from 'sinon';
55

66
const splitApi = sinon.spy();
77
const someActionSuccess = payload => ({ type: 'SOME_ACTION_SUCCESS', payload });
88
const someActionEmpty = () => ({ type: 'SOME_ACTION_EMPTY' });
99
const someActionError = error => ({ type: 'SOME_ACTION_ERROR', payload: error });
10+
const selectFilters = state => state.filters;
1011

1112
function* mySaga(input) {
1213
try {
14+
// We get the filters list from the state, using "select"
15+
const filters = yield select(selectFilters);
16+
1317
// We try to call the API, with the given input
1418
// We expect this API takes a string and returns an array of all the words, split by comma
1519
const someData = yield call(splitApi, input);
1620

17-
// From the data we get from the API, we filter out the words 'foo' and 'bar'
18-
const transformedData = someData.filter(w => ['foo', 'bar'].indexOf(w) === -1);
21+
// From the data we get from the API, we filter out the words 'foo' and 'bar' (from the list of filters we got from the state)
22+
const transformedData = someData.filter(w => filters.indexOf(w) === -1);
1923

2024
// If the resulting array is empty, we call the empty action, otherwise we call the success action
2125
if (transformedData.length === 0) {
@@ -33,6 +37,14 @@ function* mySaga(input) {
3337
// Scenario 1: When the input contains other words than foo and bar and doesn\'t throw
3438
let test = sagaHelper(mySaga('hello,foo,bar,world'), testAva);
3539

40+
test('should get the list of filters from the state', (result, t) => {
41+
t.deepEqual(result, select(selectFilters));
42+
43+
// Here we specify what the selector should have returned.
44+
// The selector is not called so we have to give its expected return value.
45+
return ['foo', 'bar'];
46+
});
47+
3648
test('should have called the mock API first, which we are going to specify the results of', (result, t) => {
3749
t.deepEqual(result, call(splitApi, 'hello,foo,bar,world'));
3850

@@ -53,6 +65,11 @@ test('and then nothing', (result, t) => {
5365
// Scenario 2: When the input only contains foo and bar
5466
test = sagaHelper(mySaga('foo,bar'), testAva);
5567

68+
test('should get the list of filters from the state', (result, t) => {
69+
t.deepEqual(result, select(selectFilters));
70+
return ['foo', 'bar'];
71+
});
72+
5673
test('should have called the mock API first, which we are going to specify the results of', (result, t) => {
5774
t.deepEqual(result, call(splitApi, 'foo,bar'));
5875
return ['foo', 'bar'];
@@ -70,6 +87,11 @@ test('and then nothing', (result, t) => {
7087
// Scenario 3: The API is broken and throws an exception
7188
test = sagaHelper(mySaga('hello,foo,bar,world'), testAva);
7289

90+
test('should get the list of filters from the state', (result, t) => {
91+
t.deepEqual(result, select(selectFilters));
92+
return ['foo', 'bar'];
93+
});
94+
7395
test('should have called the mock API first, which will throw an exception', (result, t) => {
7496
t.deepEqual(result, call(splitApi, 'hello,foo,bar,world'));
7597

jest/06.saga-using-selectors.spec.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import sagaHelper from '../main';
2+
import { call, select, put } from 'redux-saga/effects';
3+
4+
const loginAction = payload => ({ type: 'LOGIN', payload });
5+
const selectUsername = state => state.foo;
6+
7+
function* loginSaga() {
8+
const username = yield select(selectUsername);
9+
yield put(loginAction(username));
10+
}
11+
12+
describe('When testing Saga that has a selector (fake login workflow)', () => {
13+
const it = sagaHelper(loginSaga());
14+
15+
it('should select the current username from the state', result => {
16+
// Remember, we are not testing the selector here, just testing that the "select" function was called with a given selector function
17+
// The selector won't be executed (and it can't anyway, there is no state known to redux-saga on this test)
18+
expect(result).toEqual(select(selectUsername));
19+
// We return what the selector should have returned
20+
return 'donald.drumpf';
21+
});
22+
23+
it('should trigger the login action with the username we got from the state', result => {
24+
expect(result).toEqual(put(loginAction('donald.drumpf')));
25+
});
26+
27+
it('and then nothing', result => {
28+
expect(result).toBeUndefined();
29+
});
30+
});

jest/10.all-in-one-example.spec.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import sagaHelper from '../main';
2-
import { call, put } from 'redux-saga/effects';
2+
import { call, put, select } from 'redux-saga/effects';
33

44
const splitApi = jest.fn();
55
const someActionSuccess = payload => ({ type: 'SOME_ACTION_SUCCESS', payload });
66
const someActionEmpty = () => ({ type: 'SOME_ACTION_EMPTY' });
77
const someActionError = error => ({ type: 'SOME_ACTION_ERROR', payload: error });
8+
const selectFilters = state => state.filters;
89

910
function* mySaga(input) {
1011
try {
12+
// We get the filters list from the state, using "select"
13+
const filters = yield select(selectFilters);
14+
1115
// We try to call the API, with the given input
1216
// We expect this API takes a string and returns an array of all the words, split by comma
1317
const someData = yield call(splitApi, input);
1418

1519
// From the data we get from the API, we filter out the words 'foo' and 'bar'
16-
const transformedData = someData.filter(w => ['foo', 'bar'].indexOf(w) === -1);
20+
const transformedData = someData.filter(w => filters.indexOf(w) === -1);
1721

1822
// If the resulting array is empty, we call the empty action, otherwise we call the success action
1923
if (transformedData.length === 0) {
@@ -33,6 +37,14 @@ describe('When testing a complex Saga', () => {
3337
describe('Scenario 1: When the input contains other words than foo and bar and doesn\'t throw', () => {
3438
const it = sagaHelper(mySaga('hello,foo,bar,world'));
3539

40+
it('should get the list of filters from the state', result => {
41+
expect(result).toEqual(select(selectFilters));
42+
43+
// Here we specify what the selector should have returned.
44+
// The selector is not called so we have to give its expected return value.
45+
return ['foo', 'bar'];
46+
});
47+
3648
it('should have called the mock API first, which we are going to specify the results of', result => {
3749
expect(result).toEqual(call(splitApi, 'hello,foo,bar,world'));
3850

@@ -53,6 +65,11 @@ describe('When testing a complex Saga', () => {
5365
describe('Scenario 2: When the input only contains foo and bar', () => {
5466
const it = sagaHelper(mySaga('foo,bar'));
5567

68+
it('should get the list of filters from the state', result => {
69+
expect(result).toEqual(select(selectFilters));
70+
return ['foo', 'bar'];
71+
});
72+
5673
it('should have called the mock API first, which we are going to specify the results of', result => {
5774
expect(result).toEqual(call(splitApi, 'foo,bar'));
5875
return ['foo', 'bar'];
@@ -70,6 +87,11 @@ describe('When testing a complex Saga', () => {
7087
describe('Scenario 3: The API is broken and throws an exception', () => {
7188
const it = sagaHelper(mySaga('hello,foo,bar,world'));
7289

90+
it('should get the list of filters from the state', result => {
91+
expect(result).toEqual(select(selectFilters));
92+
return ['foo', 'bar'];
93+
});
94+
7395
it('should have called the mock API first, which will throw an exception', result => {
7496
expect(result).toEqual(call(splitApi, 'hello,foo,bar,world'));
7597

mocha/06.saga-using-selectors.test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import sagaHelper from '../main';
2+
import { call, select, put } from 'redux-saga/effects';
3+
import expect from 'expect.js';
4+
5+
const loginAction = payload => ({ type: 'LOGIN', payload });
6+
const selectUsername = state => state.foo;
7+
8+
function* loginSaga() {
9+
const username = yield select(selectUsername);
10+
yield put(loginAction(username));
11+
}
12+
13+
describe('When testing Saga that has a selector (fake login workflow)', () => {
14+
const it = sagaHelper(loginSaga());
15+
16+
it('should select the current username from the state', result => {
17+
// Remember, we are not testing the selector here, just testing that the "select" function was called with a given selector function
18+
// The selector won't be executed (and it can't anyway, there is no state known to redux-saga on this test)
19+
expect(result).to.eql(select(selectUsername));
20+
// We return what the selector should have returned
21+
return 'donald.drumpf';
22+
});
23+
24+
it('should trigger the login action with the username we got from the state', result => {
25+
expect(result).to.eql(put(loginAction('donald.drumpf')));
26+
});
27+
28+
it('and then nothing', result => {
29+
expect(result).to.be(undefined);
30+
});
31+
});

0 commit comments

Comments
 (0)