npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

redux-saga-integration-test

v1.4.0

Published

Utilities to test sagas, reducers and selectors in integration

Downloads

9

Readme

Utilities to test sagas, reducers and selectors in integration

Why?

Often when you're using any combination of react, redux, redux-saga, reselect you end up with the following structure for components

  • Actions file, contains simple functions that take parameters and generate an action object
  • Reducer file, contains your reducer that modifies the store depending on some actions
  • Selector file, with some selectors to get the data from the store
  • Saga file, with the side effects that implement your business logic and dispatch actions

MyComponent/index.js

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
// Local
import { doSomething } from './actions';
import { makeSelectResult } from './selectors';

export class MyComponent extends PureComponent {
  render() {
    return <div onClick={this.props.doSomething}>{this.props.result}</div>;
  }
}

export const mapStateToProps = createStructuredSelector({
  result: makeSelectResult(),
});

export function mapDispatchToProps(dispatch) {
  return {
    doSomething: () => dispatch(doSomething()),
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

MyComponent/actions.js

import {
  SOMETHING,
  SOME_OTHER_THING,
} from './constants';

export function doSomething() {
  return {
    type: SOMETHING,
  };
}

export function doSomethingElse(value) {
  return {
    type: SOME_OTHER_THING,
    value,
  };
}

MyComponent/constants.js

export const SOMETHING = 'MyComponent/SOMETHING';
export const SOME_OTHER_THING = 'MyComponent/SOME_OTHER_THING';

MyComponent/reducer.js

import { fromJS } from 'immutable';
import {
  SOMETHING,
  SOME_OTHER_THING,
} from './constants';

const initialState = fromJS({});

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case SOMETHING:
      return state.set('loading', true);
    case SOME_OTHER_THING:
      return state
        .set('value', fromJS(action.value))
        .set('loading', false);
    default:
      return state;
  }
}

MyComponent/sagas.js

import { call, takeLatest, put } from 'redux-saga/effects';
import { doSomethingElse } from './actions';
import { SOMETHING } from './constants';

export function* doTheAction(action) {
  const response = yield call(fetch, 'https://api.example.com');
  const json = yield call([response, response.json]);

  yield put(doSomethingElse(json.value));
}

export function* defaultSaga() {
  yield takeLatest(SOMETHING, doTheAction),
}

export default [defaultSaga];

MyComponent/selectors.js

import { createSelector } from 'reselect';
import { STORE_DOMAIN } from './constants';

const selectDomain = () => (state) => state.get(STORE_DOMAIN);

export const makeSelectResult = () => createSelector(
  selectDomain(),
  (state) => state.get('value')
);

Writing unit tests for each of these files is tedious and often useless.

Action creators are so trivial that don't need testing. Reducers are often simple as well, they take an action and save the value in the store. Testing selectors often implies you populate the store with your state and assert that the selected value is correct. Testing sagas is very simple with generators but often your tests are too couple to the implementation, changing the order of your calls means you'll have to change the tests effectively duplicating your work.

Even when you write unit tests for each of your files, the code might not work as expected because maybe your sagas are calling the action with the wrong order of parameters or you reducer is storing data in a different place from the selector.

A better approach is to test everything together in integration. (My opinion)

redux-saga-integration-test allows you to do just that, connect all moving parts and test the state props after dispatching action, while mocking the side effects in your sagas.

Usage

MyComponent/tests/integration.test.js

import { wire, mockedEffects } from 'redux-saga-integration-test';
import { createStructuredSelector } from 'reselect';
import { takeEvery } from 'redux-saga/effects';
import {
  STORE_DOMAIN,
  SOMETHING,
  doSomething,
} from '../constants';
import * as component from '../index';
import sagas from '../sagas';
import reducer from '../reducer';

jest.mock('redux-saga/effects', () => mockedEffects);


describe('component integration', () => {
  it('does what I expect', () => {
    const mockFetch = jest.fn(() => Promise.resolve({
      json: () => Promise.resolve({ value: 1 }),
    }));

    const { functions } = wire({
      component,
      reducer: {
        [STORE_DOMAIN]: reducer,
      },
      sagas,
      mocks: [
        [fetch, mockFetch],
      ],
    });

    return functions.doSomething().then((props) => {
      expect(mockFetch).toHaveBeenCalledWith('https://api.example.com');
      expect(props).toEqual({
        value: 1,
      });
    });
  });
});

You can also test that certain actions have been dispatched

import { wire, mockedEffects } from 'redux-saga-integration-test';
import { takeEvery, put } from 'redux-saga/effects';

jest.mock('redux-saga/effects', () => mockedEffects);

const LOAD = 'LOAD_ACTION';

/* Actions */
function putAction() {
  return { type: 'PUT_ACTION' };
}

/* Sagas */
function* putSomething(action) {
  yield put(putAction(action.value));
}
function* sagas() {
  yield takeEvery(LOAD, putSomething);
}

describe('put actions', () => {
  it('dispatches the expected action', () => {
    const { dispatch } = wire({
      sagas,
    });
    const action = { type: LOAD, value: 1 };

    return dispatch(action).then(() => {
      expect(put).toHaveBeenCalledWith(putAction(1));
    });
  });

  it('calls the expected saga', () => {
    const mockPutSomething = jest.fn();
    const { dispatch } = wire({
      sagas,
      mocks: [
        [putSomething, mockPutSomething],
      ],
    });
    const action = { type: LOAD, value: 1 };

    return dispatch(action).then(() => {
      expect(mockPutSomething).toHaveBeenCalledWith(action);
    });
  });
});

API

mockedEffects

The lines

import { mockedEffects } from 'redux-saga-integration-test';
jest.mock('redux-saga/effects', () => mockedEffects);

Allows redux-saga-integration-test to intercept your calls to redux-saga and mock the functions with side effects.

After calling jest.mock, import { put } from 'redux-saga/effects'; returns a jest mock function that you can use to assert things like expect(put).toHaveBeenCalledWith(action);;

wire

The main function is wire which takes the inputs

const { functions, dispatch, props } = wire({
  component, // Object with `mapStateToProps` and `mapDispatchToProps`
  initialStore, // Initial state loaded in the store, should be a regular object and will be converted into an immutable object
  mocks, // Array of mocked functions, see the format later
  ownProps, // Second argument passed to `mapDispatchToProps`
  params, // Shorthand version for `ownProps: { params: {} }`, useful together with react router
  reducer, // Either a function or an object used to create a combined reducer
  sagas, // Array of sagas
});

The resulting object contains

  • functions: the result of mapDispatchToProps. All functions in the object will be wrapped in a promise so you can easily access the props after calling either one of them
  • dispatch: the store dispatch function wrapped in a promise. Useful if you need to dispatch some action as a setup step before calling your functions
  • props: function returning the props computed by mapStateToProps

For example, if your mapDispatchToProps looks like

export function mapDispatchToProps(dispatch) {
  return {
    doSomething: () => dispatch(doSomething()),
  };
}

then you can call the following and receive the props after your action completes

const { functions } = wire();
functions.doSomething().then((props) => {});

mocks

The format of mocks is

[
  [originalFunction, mockedFunction],
  [anotherFunction, anotherMock],
]

sagas

The property sagas can be either

  • an array of function generators
  • an array of objects { fn: [Function generator], args: [Array of arguments] }

When using array of objects the args will be passed to sagaMiddleware when the saga is registered