Security News
Supply Chain Attack Detected in @solana/web3.js Library
A supply chain attack has been detected in versions 1.95.6 and 1.95.7 of the popular @solana/web3.js library.
redux-testkit
Advanced tools
Complete and opinionated testkit for testing Redux projects (reducers, selectors, actions, thunks)
Complete and opinionated testkit for testing Redux projects (reducers, selectors, actions, thunks)
This library mostly provides syntactic sugar and makes testing Redux fun with less boilerplate. You can naturally test all Redux constructs without this library, but you will miss out on features like automatic immutability checks.
npm install redux-testkit --save-dev
npm install jest --save-dev
import { Reducer } from 'redux-testkit';
import uut from '../reducer';
describe('counter reducer', () => {
it('should have initial state', () => {
expect(uut()).toEqual({ counter: 0 });
});
it('should handle INCREMENT action on initial state', () => {
const action = { type: 'INCREMENT' };
const result = { counter: 1 };
Reducer(uut).expect(action).toReturnState(result);
});
it('should handle INCREMENT action on existing state', () => {
const action = { type: 'INCREMENT' };
const state = { counter: 1 };
const result = { counter: 2 };
Reducer(uut).withState(state).expect(action).toReturnState(result);
});
});
A redux reducer is a pure function that takes an action object, with a type
field, and changes the state. In almost every case the state object itself must remain immutable.
Reducer(reducer).withState(state).expect(action).toReturnState(result)
Runs the reducer
on current state
providing an action
. Calling withState()
is optional, if not provided, initial state is used. Makes sure the returned state is result
.
Also verifies immutability - that state
did not mutate. Why is this important? see example bug
Reducer(reducer).withState(state).expect(action).toChangeInState(changes)
Runs the reducer
on current state
providing an action
. Calling withState()
is optional, if not provided, initial state is used. Makes sure the part that changed in the returned state matches changes
and the rest of the state hasn't changed. The format of changes
is partial state, even a deep internal object - it is compared to returned state after merging the changes with the original state (objects are deep merged, arrays are replaced).
Also verifies immutability of the state
.
The added value of this API compared to
toReturnState
is when your state object is very large and you prefer to reduce the boilerplate of preparing the entireresult
by yourself.
Reducer(reducer).withState(state).execute(action)
Runs the reducer
on current state
providing an action
. Calling withState()
is optional, if not provided, initial state is used.
Returns the returned state so you can run expectations manually. It's not recommended to use this API directly because you usually won't verify that parts in the returned state that were not supposed to change, indeed did not change.
Also verifies immutability of the state
.
The added value of this API compared to the others is that it allows you to run your own custom expectations (which isn't recommended).
import { Selector } from 'redux-testkit';
import * as uut from '../reducer';
describe('numbers selectors', () => {
it('should select integers from numbers state', () => {
const state = { numbers: [1, 2.2, 3.14, 4, 5.75, 6] };
const result = [1, 4, 6];
Selector(uut.getIntegers).expect(state).toReturn(result);
});
});
A redux selector is a pure function that takes the state and computes some derivation from it. This operation is read-only and the state object itself must not change.
Selector(selector).expect(state, ...args).toReturn(result)
Runs the selector
function on a given state
. If the selector takes more arguments, provide them at ...args
(the state is always assumed to be the first argument of a selector). Makes sure the returned result is result
.
Also verifies that state
did not mutate. Why is this important? see example bug
Selector(selector).execute(state, ...args)
Runs the selector
function on a given state
. If the selector takes more arguments, provide them at ...args
(the state is always assumed to be the first argument of a selector).
Returns the returned state so you can run expectations manually.
Also verifies that state
did not mutate.
The added value of this API compared to the others is that it allows you to run your own custom expectations.
import { Thunk } from 'redux-testkit';
import * as uut from '../actions';
describe('posts actions', () => {
it('should clear all posts', () => {
const dispatches = await Thunk(uut.clearPosts).execute();
expect(dispatches.length).toBe(1);
expect(dispatches[0].getAction()).toEqual({ type: 'POSTS_UPDATED', posts: [] });
});
it('should fetch posts from server', async () => {
jest.mock('../../../services/reddit');
const redditService = require('../../../services/reddit');
redditService.getPostsBySubreddit.mockReturnValueOnce(['post1', 'post2']);
const dispatches = await Thunk(uut.fetchPosts).execute();
expect(dispatches.length).toBe(3);
expect(dispatches[0].getAction()).toEqual({ type: 'POSTS_LOADING', loading: true });
expect(dispatches[1].getAction()).toEqual({ type: 'POSTS_UPDATED', posts: ['post1', 'post2'] });
expect(dispatches[2].getAction()).toEqual({ type: 'POSTS_LOADING', loading: false });
});
it('should filter posts', () => {
const state = { loading: false, posts: ['funny1', 'scary2', 'funny3'] };
const dispatches = await Thunk(uut.filterPosts).withState(state).execute('funny');
expect(dispatches.length).toBe(1);
expect(dispatches[0].getAction()).toEqual({ type: 'POSTS_UPDATED', posts: ['funny1', 'funny3'] });
});
});
A redux thunk wraps a synchronous or asynchronous function that performs an action. It can dispatch other actions (either plain objects or other thunks). It can also perform side effects like accessing servers.
Thunk(thunk).withState(state).execute(...args)
Runs the thunk thunk
on current state
given optional arguments ...args
. Calling withState()
is optional, no need to provide it if the internal thunk implementation doesn't call getState()
.
Returns an awaitable array of dispatches performed by the thunk (shallow, these dispatches are not executed). You can run expectations over them manually. Always await
on the result to get the actual dispatches array.
Also verifies that state
did not mutate. Why is this important? see example bug
// when a plain object action was dispatched
expect(dispatches[0].isPlainObject()).toBe(true);
expect(dispatches[0].getType()).toEqual('LOADING_CHANGED');
expect(dispatches[0].getAction()).toEqual({ type: 'LOADING_CHANGED', loading: true });
// when another thunk was dispatched
expect(dispatches[0].isFunction()).toBe(true);
expect(dispatches[0].getName()).toEqual('refreshSession'); // the function name, see note below
This is relevant when the tested thunk dispatches another thunk. In order to be able to test the name of the thunk that was dispatched, you will have to provide an explicit name to the internal anonymous function in the thunk implementation. For example:
export function refreshSession() {
return async function refreshSession(dispatch, getState) {
// ...
};
}
If the tested thunk dispatches another thunk, the other thunk is not executed. Different thunks should be considered as different units. Executing another unit should be part of an integration test, not a unit test.
If the tested thunk dispatches another thunk, you cannot set expectations on the arguments given to the other thunk. Different thunks should be considered as different units. Testing the interfaces between them should be part of an integration test, not a unit test.
If the tested thunk dispatches another thunk, you cannot mock the return value of the other thunk. Relying in your implementation on the return value of another thunk is considered bad practice. If you must test that, you should probably be changing your implementation.
These limitations may seem annoying, but they stem from best practices. If they disrupt your test, it's usually a sign of a code smell in your implementation. Fix the implementation, don't fight to test a bad practice.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { FlushThunks } from 'redux-testkit';
import * as reducers from '../reducers';
import * as postsSelectors from '../posts/reducer';
import * as uut from '../posts/actions';
describe('posts store integration', () => {
let store, flushThunks, redditService;
beforeEach(() => {
jest.resetAllMocks();
jest.resetModules();
jest.mock('../../services/reddit');
redditService = require('../../services/reddit');
// create a store with flushThunks added as the first middleware
flushThunks = FlushThunks.createMiddleware();
store = createStore(combineReducers(reducers), applyMiddleware(flushThunks, thunk));
});
it('should select posts', () => {
expect(postsSelectors.getSelectedPost(store.getState())).toEqual([]);
store.dispatch(uut.selectPost('post1'));
expect(postsSelectors.getSelectedPost(store.getState())).toEqual(['post1']);
store.dispatch(uut.selectPost('post2'));
expect(postsSelectors.getSelectedPost(store.getState())).toEqual(['post1', 'post2']);
});
it('should fetch posts from server', async () => {
redditService.getPostsBySubreddit.mockReturnValueOnce(['post1', 'post2']);
expect(postsSelectors.getPostsLoading(store.getState())).toBe(false);
expect(postsSelectors.getPosts(store.getState())).toEqual([]);
await store.dispatch(uut.fetchPosts());
expect(postsSelectors.getPostsLoading(store.getState())).toBe(false);
expect(postsSelectors.getPosts(store.getState())).toEqual(['post1', 'post2']);
});
it('should test a thunk that dispatches another thunk', async () => {
expect(postsSelectors.isForeground(store.getState())).toBe(false);
await store.dispatch(uut.initApp()); // this dispathces thunk appOnForeground
await flushThunks.flush(); // wait until all async thunks resolve
expect(postsSelectors.isForeground(store.getState())).toBe(true);
});
});
Integration test for the entire store creates a real redux store with an extra flushThunks middleware. Test starts by dispatching an action / thunk. Expectations are set over the final state using selectors.
flushThunks = FlushThunks.createMiddleware()
Creates flushThunks
middleware which should be applied to the store on creation. This middleware is useful for the case where one thunk dispatches another thunk. It allows to wait until all of the thunk promises have been resolved.
Returns a flushThunks
instance which has the following methods:
flushThunks.flush()
await
on this method to wait until all dispatched thunk promises are resolved.flushThunks.reset()
flushThunks
.This section is relevant only if you want to contribute to this library or build it locally.
npm install
npm run build
npm run test
MIT
FAQs
Complete and opinionated testkit for testing Redux projects (reducers, selectors, actions, thunks)
The npm package redux-testkit receives a total of 5,938 weekly downloads. As such, redux-testkit popularity was classified as popular.
We found that redux-testkit demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 4 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
A supply chain attack has been detected in versions 1.95.6 and 1.95.7 of the popular @solana/web3.js library.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.