0
# Testing
1
2
Tools for testing sagas in isolation, including cloneable generators and mock tasks. These utilities make it easy to test saga logic without running the full Redux-Saga middleware.
3
4
## Capabilities
5
6
### cloneableGenerator
7
8
Creates a cloneable generator from a saga function. This allows testing different branches of saga execution without replaying all the previous steps.
9
10
```typescript { .api }
11
/**
12
* Create cloneable generator for testing different saga branches
13
* @param saga - Generator function to make cloneable
14
* @returns Function that creates cloneable generator instances
15
*/
16
function cloneableGenerator<S extends Saga>(
17
saga: S
18
): (...args: Parameters<S>) => SagaIteratorClone;
19
20
interface SagaIteratorClone extends SagaIterator {
21
/** Create clone at current execution point */
22
clone(): SagaIteratorClone;
23
}
24
```
25
26
**Usage Examples:**
27
28
```typescript
29
import { cloneableGenerator } from "@redux-saga/testing-utils";
30
import { call, put, select } from "redux-saga/effects";
31
32
function* userSaga(action) {
33
const userId = action.payload.userId;
34
const user = yield call(api.fetchUser, userId);
35
36
const isAdmin = yield select(getIsAdmin, user.id);
37
38
if (isAdmin) {
39
yield put({ type: 'ADMIN_USER_LOADED', user });
40
} else {
41
yield put({ type: 'REGULAR_USER_LOADED', user });
42
}
43
}
44
45
// Test different branches without replaying setup
46
describe('userSaga', () => {
47
it('handles admin and regular users', () => {
48
const generator = cloneableGenerator(userSaga)({
49
payload: { userId: 123 }
50
});
51
52
// Common setup
53
assert.deepEqual(
54
generator.next().value,
55
call(api.fetchUser, 123)
56
);
57
58
const mockUser = { id: 123, name: 'John' };
59
assert.deepEqual(
60
generator.next(mockUser).value,
61
select(getIsAdmin, 123)
62
);
63
64
// Clone at decision point
65
const adminClone = generator.clone();
66
const regularClone = generator.clone();
67
68
// Test admin branch
69
assert.deepEqual(
70
adminClone.next(true).value,
71
put({ type: 'ADMIN_USER_LOADED', user: mockUser })
72
);
73
74
// Test regular user branch
75
assert.deepEqual(
76
regularClone.next(false).value,
77
put({ type: 'REGULAR_USER_LOADED', user: mockUser })
78
);
79
});
80
});
81
```
82
83
### createMockTask
84
85
Creates a mock task object for testing saga interactions with forked tasks. The mock task can be configured to simulate different task states and results.
86
87
```typescript { .api }
88
/**
89
* Create mock task for testing saga task interactions
90
* @returns MockTask that can be configured for different scenarios
91
*/
92
function createMockTask(): MockTask;
93
94
interface MockTask extends Task {
95
/** @deprecated Use setResult, setError, or cancel instead */
96
setRunning(running: boolean): void;
97
/** Set successful result for the task */
98
setResult(result: any): void;
99
/** Set error result for the task */
100
setError(error: any): void;
101
/** Standard task methods */
102
isRunning(): boolean;
103
result<T = any>(): T | undefined;
104
error(): any | undefined;
105
toPromise<T = any>(): Promise<T>;
106
cancel(): void;
107
setContext(props: object): void;
108
}
109
```
110
111
**Usage Examples:**
112
113
```typescript
114
import { createMockTask } from "@redux-saga/testing-utils";
115
import { fork, join, cancel } from "redux-saga/effects";
116
117
function* coordinatorSaga() {
118
const task1 = yield fork(workerSaga1);
119
const task2 = yield fork(workerSaga2);
120
121
try {
122
const results = yield join([task1, task2]);
123
yield put({ type: 'ALL_TASKS_COMPLETED', results });
124
} catch (error) {
125
yield cancel([task1, task2]);
126
yield put({ type: 'TASKS_FAILED', error });
127
}
128
}
129
130
describe('coordinatorSaga', () => {
131
it('handles successful task completion', () => {
132
const generator = coordinatorSaga();
133
134
// Mock the fork effects
135
generator.next(); // first fork
136
generator.next(); // second fork
137
138
// Create mock tasks
139
const mockTask1 = createMockTask();
140
const mockTask2 = createMockTask();
141
142
// Set successful results
143
mockTask1.setResult('result1');
144
mockTask2.setResult('result2');
145
146
// Test join behavior
147
const joinEffect = generator.next([mockTask1, mockTask2]).value;
148
assert.deepEqual(joinEffect, join([mockTask1, mockTask2]));
149
150
// Verify final action
151
const putEffect = generator.next(['result1', 'result2']).value;
152
assert.deepEqual(putEffect, put({
153
type: 'ALL_TASKS_COMPLETED',
154
results: ['result1', 'result2']
155
}));
156
});
157
158
it('handles task failures', () => {
159
const generator = coordinatorSaga();
160
161
generator.next(); // first fork
162
generator.next(); // second fork
163
164
const mockTask1 = createMockTask();
165
const mockTask2 = createMockTask();
166
167
// Set error on one task
168
mockTask1.setError(new Error('Task failed'));
169
170
// Test error handling
171
const error = new Error('Task failed');
172
const cancelEffect = generator.throw(error).value;
173
174
assert.deepEqual(cancelEffect, cancel([mockTask1, mockTask2]));
175
});
176
});
177
```
178
179
## Testing Patterns
180
181
### Basic Saga Testing
182
183
```typescript
184
import { call, put } from "redux-saga/effects";
185
186
function* fetchUserSaga(action) {
187
try {
188
const user = yield call(api.fetchUser, action.payload.id);
189
yield put({ type: 'FETCH_USER_SUCCESS', user });
190
} catch (error) {
191
yield put({ type: 'FETCH_USER_FAILURE', error: error.message });
192
}
193
}
194
195
describe('fetchUserSaga', () => {
196
it('fetches user successfully', () => {
197
const action = { payload: { id: 1 } };
198
const generator = fetchUserSaga(action);
199
200
// Test API call
201
assert.deepEqual(
202
generator.next().value,
203
call(api.fetchUser, 1)
204
);
205
206
// Test success action
207
const user = { id: 1, name: 'John' };
208
assert.deepEqual(
209
generator.next(user).value,
210
put({ type: 'FETCH_USER_SUCCESS', user })
211
);
212
213
// Test completion
214
assert.equal(generator.next().done, true);
215
});
216
217
it('handles API errors', () => {
218
const action = { payload: { id: 1 } };
219
const generator = fetchUserSaga(action);
220
221
generator.next(); // Skip to after call
222
223
// Throw error
224
const error = new Error('API Error');
225
assert.deepEqual(
226
generator.throw(error).value,
227
put({ type: 'FETCH_USER_FAILURE', error: 'API Error' })
228
);
229
});
230
});
231
```
232
233
### Testing with runSaga
234
235
```typescript
236
import { runSaga } from "redux-saga";
237
238
describe('saga integration tests', () => {
239
it('dispatches correct actions', async () => {
240
const dispatched = [];
241
const mockStore = {
242
getState: () => ({ user: { id: 1 } }),
243
dispatch: (action) => dispatched.push(action)
244
};
245
246
await runSaga(mockStore, fetchUserSaga, { payload: { id: 1 } }).toPromise();
247
248
expect(dispatched).toContainEqual({
249
type: 'FETCH_USER_SUCCESS',
250
user: { id: 1, name: 'John' }
251
});
252
});
253
});
254
```
255
256
### Testing Channels
257
258
```typescript
259
import { channel } from "redux-saga";
260
import { take, put } from "redux-saga/effects";
261
262
function* channelSaga() {
263
const chan = yield call(channel);
264
yield fork(producer, chan);
265
yield fork(consumer, chan);
266
}
267
268
describe('channelSaga', () => {
269
it('creates and uses channel', () => {
270
const generator = channelSaga();
271
272
// Test channel creation
273
assert.deepEqual(
274
generator.next().value,
275
call(channel)
276
);
277
278
// Mock channel
279
const mockChannel = { put: jest.fn(), take: jest.fn() };
280
281
// Test producer fork
282
assert.deepEqual(
283
generator.next(mockChannel).value,
284
fork(producer, mockChannel)
285
);
286
287
// Test consumer fork
288
assert.deepEqual(
289
generator.next().value,
290
fork(consumer, mockChannel)
291
);
292
});
293
});
294
```
295
296
### Testing Error Boundaries
297
298
```typescript
299
function* sagaWithErrorHandling() {
300
try {
301
yield call(riskyOperation);
302
yield put({ type: 'SUCCESS' });
303
} catch (error) {
304
yield put({ type: 'ERROR', error: error.message });
305
} finally {
306
yield put({ type: 'CLEANUP' });
307
}
308
}
309
310
describe('error handling', () => {
311
it('handles errors and runs cleanup', () => {
312
const generator = sagaWithErrorHandling();
313
314
generator.next(); // Skip to call
315
316
// Throw error
317
const error = new Error('Operation failed');
318
const errorAction = generator.throw(error).value;
319
assert.deepEqual(errorAction, put({
320
type: 'ERROR',
321
error: 'Operation failed'
322
}));
323
324
// Test finally block
325
const cleanupAction = generator.next().value;
326
assert.deepEqual(cleanupAction, put({ type: 'CLEANUP' }));
327
});
328
});
329
```
330
331
## Testing Best Practices
332
333
1. **Test individual effects**: Verify each yielded effect matches expected values
334
2. **Use cloneableGenerator**: Test different branches without duplicating setup
335
3. **Mock external dependencies**: Use mock tasks and channels for isolation
336
4. **Test error paths**: Verify error handling using generator.throw()
337
5. **Test integration**: Use runSaga for end-to-end saga testing
338
6. **Assert completion**: Check generator.next().done for saga completion