Security News
Fluent Assertions Faces Backlash After Abandoning Open Source Licensing
Fluent Assertions is facing backlash after dropping the Apache license for a commercial model, leaving users blindsided and questioning contributor rights.
redux-saga-test-plan
Advanced tools
redux-saga-test-plan is a testing utility for redux-saga, which allows you to test your sagas in a more declarative and less boilerplate-heavy way. It provides tools to test the effects and the flow of your sagas, making it easier to ensure that your asynchronous logic behaves as expected.
Unit Testing Sagas
This feature allows you to unit test individual sagas by asserting that certain effects (like call, put, etc.) are yielded. The example demonstrates testing a saga that calls an API and returns the response.
const { expectSaga } = require('redux-saga-test-plan');
const { call } = require('redux-saga/effects');
function* fetchData(api) {
const response = yield call(api);
return response;
}
expectSaga(fetchData, api)
.call(api)
.returns({ data: 'test' })
.run();
Integration Testing Sagas
This feature allows you to perform integration tests by running the saga with a reducer and asserting the final state. The example shows testing a saga that fetches data and updates the state based on the result.
const { expectSaga } = require('redux-saga-test-plan');
const { call, put } = require('redux-saga/effects');
const { combineReducers } = require('redux');
function* fetchData(api) {
try {
const response = yield call(api);
yield put({ type: 'FETCH_SUCCESS', payload: response });
} catch (error) {
yield put({ type: 'FETCH_FAILURE', error });
}
}
const reducer = combineReducers({
data: (state = null, action) => {
switch (action.type) {
case 'FETCH_SUCCESS':
return action.payload;
default:
return state;
}
}
});
expectSaga(fetchData, api)
.withReducer(reducer)
.provide([[call(api), { data: 'test' }]])
.hasFinalState({ data: { data: 'test' } })
.run();
Mocking Effects
This feature allows you to mock the effects that your saga yields, making it easier to test sagas in isolation. The example demonstrates mocking an API call within a saga.
const { expectSaga } = require('redux-saga-test-plan');
const { call } = require('redux-saga/effects');
function* fetchData(api) {
const response = yield call(api);
return response;
}
expectSaga(fetchData, api)
.provide([[call(api), { data: 'mocked' }]])
.returns({ data: 'mocked' })
.run();
redux-saga-test is another library for testing redux-sagas. It provides a more manual approach compared to redux-saga-test-plan, requiring you to manually step through the saga and assert each effect. This can be more flexible but also more verbose.
redux-saga-tester is a utility for testing redux-sagas by running them in a controlled environment and asserting the resulting actions and state changes. It is similar to redux-saga-test-plan but focuses more on end-to-end testing of sagas within a redux store.
Redux Saga Test Plan makes testing sagas a breeze. Whether you need to test
exact effects and their ordering or just test your saga put
's a specific
action at some point, Redux Saga Test Plan has you covered.
Redux Saga Test Plan aims to embrace both integration testing and unit testing approaches to make testing your sagas easy.
Requires global Promise
to be available
One downside to unit testing sagas is that it couples your test to your
implementation. Simple reordering of yielded effects in your saga could break
your tests even if the functionality stays the same. If you're not concerned
with the order or exact effects your saga yields, then you can take an
integrative approach, testing the behavior of your saga when run by Redux Saga.
Then, you can simply test that a particular effect was yielded during the saga
run. For this, use the expectSaga
test function.
Import the expectSaga
function and pass in your saga function as an argument.
Any additional arguments to expectSaga
will become arguments to the saga
function. The return value is a chainable API with assertions for the different
effect creators available in Redux Saga.
In the example below, we test that the userSaga
successfully put
s a
RECEIVE_USER
action with the fakeUser
as the payload. We call expectSaga
with the userSaga
and supply an api
object as an argument to userSaga
. We
assert the expected put
effect via the put
assertion method. Then, we call
the dispatch
method with a REQUEST_USER
action that contains the user id
payload. The dispatch
method will supply actions to take
effects. Finally,
we start the test by calling the run
method which returns a Promise
. Tests
with expectSaga
will always run asynchronously, so the returned Promise
resolves when the saga finishes or when expectSaga
forces a timeout. If you're
using a test runner like Jest, you can return the Promise
inside your Jest
test so Jest knows when the test is complete.
import { call, put, take } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
function* userSaga(api) {
const action = yield take('REQUEST_USER');
const user = yield call(api.fetchUser, action.payload);
yield put({ type: 'RECEIVE_USER', payload: user });
}
it('just works!', () => {
const api = {
fetchUser: id => ({ id, name: 'Tucker' }),
};
return expectSaga(userSaga, api)
// Assert that the `put` will eventually happen.
.put({
type: 'RECEIVE_USER',
payload: { id: 42, name: 'Tucker' },
})
// Dispatch any actions that the saga will `take`.
.dispatch({ type: 'REQUEST_USER', payload: 42 })
// Start the test. Returns a Promise.
.run();
});
expectSaga
runs your saga with Redux Saga, so it will try to resolve effects
just like Redux Saga would in your application. This is great for integration
testing, but sometimes it can be laborious to bootstrap your entire application
for tests or mock things like server APIs. In those cases, you can use
providers which are perfect for mocking values directly with expectSaga
.
Providers are similar to middleware that allow you to intercept effects before
they reach Redux Saga. You can choose to return a mock value instead of allowing
Redux Saga to handle the effect, or you can pass on the effect to other
providers or eventually Redux Saga.
expectSaga
has two flavors of providers, static providers and dynamic
providers. Static providers are easier to compose and reuse, but dynamic
providers give you more flexibility with non-deterministic effects. Here is one
example below using static providers. There are more examples of providers in
the
docs.
import { call, put, take } 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';
import api from 'my-api';
function* userSaga(api) {
try {
const action = yield take('REQUEST_USER');
const user = yield call(api.fetchUser, action.payload);
const pet = yield call(api.fetchPet, user.petId);
yield put({
type: 'RECEIVE_USER',
payload: { user, pet },
});
} catch (e) {
yield put({ type: 'FAIL_USER', error: e });
}
}
it('fetches the user', () => {
const fakeUser = { name: 'Jeremy', petId: 20 };
const fakeDog = { name: 'Tucker' };
return expectSaga(userSaga, api)
.provide([
[call(api.fetchUser, 42), fakeUser],
[matchers.call.fn(api.fetchPet), fakeDog],
])
.put({
type: 'RECEIVE_USER',
payload: { user: fakeUser, pet: fakeDog },
})
.dispatch({ type: 'REQUEST_USER', payload: 42 })
.run();
});
it('handles errors', () => {
const error = new Error('error');
return expectSaga(userSaga, api)
.provide([
[matchers.call.fn(api.fetchUser), throwError(error)]
])
.put({ type: 'FAIL_USER', error })
.dispatch({ type: 'REQUEST_USER', payload: 42 })
.run();
});
Notice we pass in an array of tuple pairs (or array pairs) that contain a
matcher and a fake value. You can use the effect creators from Redux Saga or
matchers from the redux-saga-test-plan/matchers
module to match effects. The
bonus of using Redux Saga Test Plan's matchers is that they offer special
partial matchers like call.fn
which matches by the function without worrying
about the specific args
contained in the actual call
effect. Notice in the
second test that we can also simulate errors with the throwError
function from
the redux-saga-test-plan/providers
module. This is perfect for simulating
server problems.
One good use case for integration testing is testing your reducer too. You can
hook up your reducer to your test by calling the withReducer
method with your
reducer function.
import { put } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';
const initialDog = {
name: 'Tucker',
age: 11,
};
function reducer(state = initialDog, action) {
if (action.type === 'HAVE_BIRTHDAY') {
return {
...state,
age: state.age + 1,
};
}
return state;
}
function* saga() {
yield put({ type: 'HAVE_BIRTHDAY' });
}
it('handles reducers and store state', () => {
return expectSaga(saga)
.withReducer(reducer)
.hasFinalState({
name: 'Tucker',
age: 12, // <-- age changes in store state
})
.run();
});
If you want to ensure that your saga yields specific types of effects in a
particular order, then you can use the testSaga
function. Here's a simple
example:
import { testSaga } from 'redux-saga-test-plan';
function identity(value) {
return value;
}
function* mainSaga(x, y) {
const action = yield take('HELLO');
yield put({ type: 'ADD', payload: x + y });
yield call(identity, action);
}
const action = { type: 'TEST' };
it('works with unit tests', () => {
testSaga(mainSaga, 40, 2)
// advance saga with `next()`
.next()
// assert that the saga yields `take` with `'HELLO'` as type
.take('HELLO')
// pass back in a value to a saga after it yields
.next(action)
// assert that the saga yields `put` with the expected action
.put({ type: 'ADD', payload: 42 })
.next()
// assert that the saga yields a `call` to `identity` with
// the `action` argument
.call(identity, action)
.next()
// assert that the saga is finished
.isDone();
});
To see large effect objects while Expected & Actual result comparison you'll need to extend inspect options. Example:
import util from 'util';
import testSaga from 'redux-saga-test-plan';
import { testableSaga } from '../sagas';
describe('Some sagas to test', () => {
util.inspect.defaultOptions.depth = null;
it('testableSaga', () => {
testSaga(testableSaga)
.next()
.put({ /* large object here */ })
.next()
.isDone();
});
});
yarn add redux-saga-test-plan --dev
npm install --save-dev redux-saga-test-plan
v4.0.6
FAQs
Test Redux Saga with an easy plan
The npm package redux-saga-test-plan receives a total of 137,105 weekly downloads. As such, redux-saga-test-plan popularity was classified as popular.
We found that redux-saga-test-plan demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Fluent Assertions is facing backlash after dropping the Apache license for a commercial model, leaving users blindsided and questioning contributor rights.
Research
Security News
Socket researchers uncover the risks of a malicious Python package targeting Discord developers.
Security News
The UK is proposing a bold ban on ransomware payments by public entities to disrupt cybercrime, protect critical services, and lead global cybersecurity efforts.