• Stars
    star
    1,248
  • Rank 37,649 (Top 0.8 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 8 years ago
  • Updated about 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Test Redux Saga with an easy plan.

Redux Saga Test Plan

npm Travis branch Codecov

Test Redux Saga with an easy plan.

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.

Table of Contents

Documentation

Integration Testing

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.

Simple Example

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 puts 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();
});

Mocking with Providers

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.

Example with Reducer

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();
});

Unit Testing

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();
});

Extending inspect options

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();
  });
});

Install

yarn add redux-saga-test-plan --dev
npm install --save-dev redux-saga-test-plan

More Repositories

1

revalidate

Elegant and composable validations
JavaScript
363
star
2

chroma

Ruby gem for color manipulation and palette generation
Ruby
232
star
3

redux-saga-router

A router for Redux Saga
JavaScript
152
star
4

run-elm

Run Elm code from the command line
JavaScript
52
star
5

marionette.component

Manage and create components for your Marionette.js application
JavaScript
36
star
6

fp-basics-in-es6

Code samples for "Functional Programming Basics in ES6" talk
JavaScript
35
star
7

redux-resource

Redux action creator for managing RESTful resources
JavaScript
32
star
8

combos

Generate all possible permutations of an object's key-value pairs
JavaScript
27
star
9

rise-of-async-js-talk

Code samples, demos, and resources for "The Rise of Async JavaScript" talk
JavaScript
26
star
10

react-bind-closures

Bind closures to stateless React components to avoid creating closures at render time
JavaScript
22
star
11

programming-elm.com

Source for programming-elm.com
Elm
19
star
12

arch-elm

Code for talk "Toward a Better Front-end Architecture: Elm"
Elm
19
star
13

fsm-iterator

A finite state machine iterator for JavaScript
JavaScript
16
star
14

website

Source for my personal website
JavaScript
15
star
15

marionette.polymerview

JavaScript
13
star
16

effective-react-testing

JavaScript
12
star
17

whitesimilarity

Ruby gem implementation of the White Similarity Algorithm
C
12
star
18

building-web-apps-with-elm-tutorial

Elm
11
star
19

perchance

A simple maybe monad for JavaScript
JavaScript
10
star
20

elm-workshop

Setup instructions and finished demos for the "Building Web Apps with Elm" workshop
9
star
21

redux-binary

Redux reducer and actions for handling binary state
JavaScript
9
star
22

react-classify

Classify functional React components to use component lifecycle hooks
JavaScript
9
star
23

elm-stream

Fast and simple stream library for Elm
Elm
8
star
24

dotfiles

Vim Script
7
star
25

redux-glue

Glue together Redux actions to create testable, sequenced actions
JavaScript
6
star
26

public-apis

Simple web app for finding public APIs
JavaScript
6
star
27

photomosaic

Web app for computing photomosaics in the browser
JavaScript
5
star
28

building-resilient-api-driven-elm

Elm
5
star
29

babel-plugin-transform-underscore-arrow-functions

Transform arrow functions with an underscore param into param-less functions
JavaScript
4
star
30

react-revalidate

Validate React component props with revalidate
JavaScript
4
star
31

elm-demos

Example Elm demo apps to complement future Elm blog posts
Elm
3
star
32

conways-game-of-life

Conway's Game of Life implemented in Elm
Elm
3
star
33

migrate-js-to-elm

JavaScript
3
star
34

meteor-rps

JavaScript
2
star
35

advanced-cypress-testing

JavaScript
2
star
36

_redux-saga-router_5

JavaScript
2
star
37

redux-revalidate

Validate your Redux store state with revalidate
JavaScript
2
star
38

measurb

Handle units of measurement in Ruby with ease!
Ruby
2
star
39

fp-in-javascript

Many common functional programming patterns succinctly implemented in JavaScript
JavaScript
2
star
40

modular-react-and-redux-talk

Code samples for the "Modular React and Redux" talk
JavaScript
2
star
41

marionette.attrsync

JavaScript
1
star
42

node-koa-es6

JavaScript
1
star
43

backbone.directattributes

Direct attributes for Backbone models
1
star
44

pat-packet-visits-ruby-rails

Ruby
1
star
45

playit

Control audio players on websites with your keyboard's Play/Pause key
JavaScript
1
star
46

state-side-effects-and-redux

Code samples for "State, Side Effects, and Redux. Oh my!" talk
JavaScript
1
star
47

es7-and-beyond-talk

JavaScript
1
star
48

bundle-with-webpack-talk

Resources for the talk "Bundle the Web with Webpack"
JavaScript
1
star
49

dynamically-sassy-demos

Ruby
1
star
50

orchestrating-apps-angular

JavaScript
1
star