Skip to main content

Redux Saga Testing

// Redux Saga Testing Cheatsheet

// 1. Basic Test Setup
import { runSaga } from 'redux-saga';
import { call, put, select } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
import * as matchers from 'redux-saga-test-plan/matchers';
import { throwError } from 'redux-saga-test-plan/providers';

// 2. Simple Saga Example
function* fetchUserSaga(action) {
try {
const user = yield call(api.fetchUser, action.payload.id);
yield put({ type: 'FETCH_USER_SUCCESS', payload: user });
} catch (error) {
yield put({ type: 'FETCH_USER_ERROR', error });
}
}

// 3. Testing with redux-saga-test-plan
describe('fetchUserSaga', () => {
const userId = 1;
const user = { id: 1, name: 'John' };

// Basic happy path test
test('should fetch user successfully', () => {
return expectSaga(fetchUserSaga, { payload: { id: userId } })
.provide([
[matchers.call.fn(api.fetchUser), user]
])
.put({ type: 'FETCH_USER_SUCCESS', payload: user })
.run();
});

// Error path test
test('should handle errors', () => {
const error = new Error('User not found');

return expectSaga(fetchUserSaga, { payload: { id: userId } })
.provide([
[matchers.call.fn(api.fetchUser), throwError(error)]
])
.put({ type: 'FETCH_USER_ERROR', error })
.run();
});
});

// 4. Testing Complex Flows
function* complexSaga() {
const user = yield select(state => state.user);
yield call(api.trackUserAction, user.id);
const data = yield call(api.fetchData);
yield put({ type: 'DATA_LOADED', payload: data });
}

test('complex saga flow', () => {
const fakeUser = { id: 1 };
const fakeData = { items: [] };

return expectSaga(complexSaga)
.withState({
user: fakeUser
})
.provide([
[matchers.call.fn(api.trackUserAction), null],
[matchers.call.fn(api.fetchData), fakeData]
])
.call(api.trackUserAction, fakeUser.id)
.put({ type: 'DATA_LOADED', payload: fakeData })
.run();
});

// 5. Testing Saga Forks and Parallel Tasks
function* parentSaga() {
yield fork(childSaga1);
yield fork(childSaga2);
}

test('forked sagas', () => {
return expectSaga(parentSaga)
.provide([
// Provide mocked values for child sagas
])
.fork(childSaga1)
.fork(childSaga2)
.run();
});

// 6. Testing Race Conditions
function* raceSaga() {
const { response, timeout } = yield race({
response: call(api.fetchData),
timeout: delay(1000)
});
}

test('race condition', () => {
const response = { data: 'success' };

return expectSaga(raceSaga)
.provide([
[matchers.race({
response: matchers.call.fn(api.fetchData),
timeout: matchers.call.fn(delay)
}), { response }]
])
.run();
});

// 7. Testing Take Effects
function* watchUserSaga() {
while (true) {
const action = yield take('USER_REQUEST');
yield call(handleUserRequest, action);
}
}

test('watch saga', () => {
const action = { type: 'USER_REQUEST', payload: 1 };

return expectSaga(watchUserSaga)
.take('USER_REQUEST')
.dispatch(action)
.call(handleUserRequest, action)
.silentRun();
});

// 8. Common Patterns and Best Practices

// Pattern 1: Dynamic Providers
test('dynamic providers', () => {
return expectSaga(mySaga)
.provide({
call: (effect, next) => {
if (effect.fn === api.fetchUser) {
return { id: 1, name: 'John' };
}
return next();
}
})
.run();
});

// Pattern 2: Testing Saga Cancellation
test('saga cancellation', () => {
return expectSaga(mySaga)
.provide([
[matchers.fork.fn(backgroundTask), true]
])
.fork(backgroundTask)
.cancel.like({ pattern: backgroundTask })
.run();
});

// Pattern 3: Testing with Partial State
test('partial state matching', () => {
return expectSaga(mySaga)
.withState({
user: { id: 1 },
// Other state properties can be omitted
})
.run();
});

// 9. Helper Functions for Testing
const createSagaTestHelper = (initialState = {}) => {
const dispatched = [];

return {
dispatch: (action) => dispatched.push(action),
getState: () => initialState,
getDispatched: () => dispatched
};
};

// 10. Manual Testing without redux-saga-test-plan
test('manual saga testing', async () => {
const dispatched = [];
const fakeStore = {
dispatch: (action) => dispatched.push(action),
getState: () => ({ user: { id: 1 } })
};

await runSaga(
fakeStore,
mySaga,
{ type: 'START_SAGA' }
).toPromise();

expect(dispatched).toEqual([
// Expected actions
]);
});