All Projects → antoinejaussoin → Redux Saga Testing

antoinejaussoin / Redux Saga Testing

Licence: mit
A no-brainer way of testing your Sagas

Programming Languages

typescript
32286 projects

Projects that are alternatives of or similar to Redux Saga Testing

eslint-config-adjunct
A reasonable collection of plugins to use alongside your main esLint configuration
Stars: ✭ 39 (-74%)
Mutual labels:  mocha, redux-saga, jest, ava
Public
Repository for wallaby.js questions and issues
Stars: ✭ 662 (+341.33%)
Mutual labels:  jest, mocha, ava
Istanbuljs
monorepo containing the various nuts and bolts that facilitate istanbul.js test instrumentation
Stars: ✭ 656 (+337.33%)
Mutual labels:  jest, mocha, ava
Jest Codemods
Codemods for migrating to Jest https://github.com/facebook/jest 👾
Stars: ✭ 731 (+387.33%)
Mutual labels:  jest, mocha, ava
awesome-javascript-testing
🔧 Awesome JavaScript testing resources
Stars: ✭ 28 (-81.33%)
Mutual labels:  mocha, jest, ava
React Native Navigation Redux Starter Kit
React Native Navigation(v2) Starter Kit with Redux, Saga, ESLint, Babel, Jest and Facebook SDK 😎
Stars: ✭ 271 (+80.67%)
Mutual labels:  sagas, jest, redux-saga
Rxjs Marbles
An RxJS marble testing library for any test framework
Stars: ✭ 267 (+78%)
Mutual labels:  jest, mocha, ava
Enzyme
JavaScript Testing utilities for React
Stars: ✭ 19,781 (+13087.33%)
Mutual labels:  jest, mocha, ava
Real Estate App Ui
Real Estate Property Listing App Built With React Native
Stars: ✭ 139 (-7.33%)
Mutual labels:  jest, redux-saga
React Redux Boilerplate
A minimal React-Redux boilerplate with all the best practices
Stars: ✭ 799 (+432.67%)
Mutual labels:  jest, redux-saga
Suman
🌇 🌆 🌉 Advanced, user-friendly, language-agnostic, super-high-performance test runner. http://sumanjs.org
Stars: ✭ 57 (-62%)
Mutual labels:  mocha, ava
Fullstack Shopping Cart
MERN stack shopping cart, written in TypeScript
Stars: ✭ 82 (-45.33%)
Mutual labels:  jest, redux-saga
Redux Saga
An alternative side effect model for Redux apps
Stars: ✭ 21,975 (+14550%)
Mutual labels:  sagas, redux-saga
React Adventure
⛰ React high-ending architecture & patterns ready for use. Made for big and small projects. PWA Ready.
Stars: ✭ 62 (-58.67%)
Mutual labels:  jest, redux-saga
Ts React Boilerplate
Universal React App with Redux 4, Typescript 3, and Webpack 4
Stars: ✭ 104 (-30.67%)
Mutual labels:  jest, redux-saga
React Redux Saga Starter
Basic, Opinionated starter kit for React+Redux+Redux Saga with support for SCSS CSS Modules, Storybook, JEST testing, and ESLint
Stars: ✭ 12 (-92%)
Mutual labels:  jest, redux-saga
Starter Lapis
Cutting edge starter kit
Stars: ✭ 72 (-52%)
Mutual labels:  jest, redux-saga
Cooky Cutter
🍪 Object factories for testing in TypeScript
Stars: ✭ 109 (-27.33%)
Mutual labels:  jest, mocha
What The Splash
Tutorial for building an unsplash image gallery with redux saga :atom:
Stars: ✭ 107 (-28.67%)
Mutual labels:  jest, redux-saga
Reeakt
A modern React boilerplate to awesome web applications
Stars: ✭ 116 (-22.67%)
Mutual labels:  jest, redux-saga

redux-saga-testing


A no-brainer way of testing your Sagas™®


Examples include Jest, Mocha and AVA

npm Travis branch Known Vulnerabilities

Sagas are hard, testing them is even harder (Napoleon)

Testing Sagas is difficult, and the aim of this little utility is to make testing them as close as possible to testing regular code.

It should work with your favourite testing framework, although in this README the examples are using Jest.

You can find examples for ava as well in the GitHub repository:

Where are the Mocha examples gone? Since the migration to TypeScript, and because I can't have typings for both Jest and Mocha at the same time (they conflict with each other), I removed all Mocha examples. The library still works with Mocha though.

How to use

  • Simply import the helper by doing import sagaHelper from 'redux-saga-testing';
  • Override your "it" testing function with the wrapper: const it = sagaHelper(sagaUnderTest())
  • Add one "it" per iteration to to test each step (see examples below to see how it works)

Dependencies

The helper doesn't depend on anything.

You can then use this with any version of redux-saga, or without it for that matter and test simple generators.

How to run the tests

  • Checkout the code: git clone https://github.com/antoinejaussoin/redux-saga-testing.git
  • Install the dependencies: npm i or yarn
  • Run the tests: npm test

Tutorial

This tutorial goes from simple to complex. As you will see, testing Sagas becomes as easy as testing regular (and synchronous) code.

Simple (non-Saga) examples

This example uses a simple generator. This is not using any of the redux-saga functions and helpers.

import sagaHelper from 'redux-saga-testing';

// This is the generator / saga you wish to test
function* myGenerator() {
  yield 42;
  yield 43;
  yield 44;
}

describe('When testing a very simple generator (not even a Saga)', () => {
  // You start by overidding the "it" function of your test framework, in this scope.
  // That way, all the tests after that will look like regular tests but will actually be
  // running the generator forward at each step.
  // All you have to do is to pass your generator and call it.
  const it = sagaHelper(myGenerator());

  // This looks very much like a normal "it", with one difference: the function is passed
  // a "result", which is what has been yield by the generator.
  // This is what you are going to test, using your usual testing framework syntax.
  // Here we are using "expect" because we are using Jest, but really it could be anything.
  it('should return 42', (result) => {
    expect(result).toBe(42);
  });

  // On the next "it", we move the generator forward one step, and test again.
  it('and then 43', (result) => {
    expect(result).toBe(43);
  });

  // Same here
  it('and then 44', (result) => {
    expect(result).toBe(44);
  });

  // Now the generator doesn't yield anything, so we can test we arrived at the end
  it('and then nothing', (result) => {
    expect(result).toBeUndefined();
  });
});

Testing a simple Saga

This examples is now actually using the redux-saga utility functions.

The important point to note here, is that Sagas describe what happens, they don't actually act on it. For example, an API will never be called, you don't have to mock it, when using call. Same thing for a selector, you don't need to mock the state when using yield select(mySelector).

This makes testing Sagas very easy indeed.

import sagaHelper from 'redux-saga-testing';
import { call, put } from 'redux-saga/effects';

const api = jest.fn();
const someAction = () => ({ type: 'SOME_ACTION', payload: 'foo' });

function* mySaga() {
  yield call(api);
  yield put(someAction());
}

describe('When testing a very simple Saga', () => {
  const it = sagaHelper(mySaga());

  it('should have called the mock API first', (result) => {
    // Here we test that the generator did run the "call" function, with the "api" as an argument.
    // The api funtion is NOT called.
    expect(result).toEqual(call(api));

    // It's very important to understand that the generator ran the 'call' function,
    // which only describes what it does, and that the API itself is never called.
    // This is what we are testing here: (but you don't need to test that in your own tests)
    expect(api).not.toHaveBeenCalled();
  });

  it('and then trigger an action', (result) => {
    // We then test that on the next step some action is called
    // Here, obviously, 'someAction' is called but it doesn't have any effect
    // since it only returns an object describing the action
    expect(result).toEqual(put(someAction()));
  });

  it('and then nothing', (result) => {
    expect(result).toBeUndefined();
  });
});

Testing a complex Saga

This example deals with pretty much all use-cases for using Sagas, which involves using a selector, calling an API, getting exceptions, have some conditional logic based on some inputs and puting new actions.

import sagaHelper from 'redux-saga-testing';
import { call, put, select } from 'redux-saga/effects';

const splitApi = jest.fn();
const someActionSuccess = (payload: any) => ({
  type: 'SOME_ACTION_SUCCESS',
  payload,
});
const someActionEmpty = () => ({ type: 'SOME_ACTION_EMPTY' });
const someActionError = (error: any) => ({
  type: 'SOME_ACTION_ERROR',
  payload: error,
});
const selectFilters = (state: any) => state.filters;

function* mySaga(input) {
  try {
    // We get the filters list from the state, using "select"
    const filters = yield select(selectFilters);

    // We try to call the API, with the given input
    // We expect this API takes a string and returns an array of all the words, split by comma
    const someData = yield call(splitApi, input);

    // From the data we get from the API, we filter out the words 'foo' and 'bar'
    const transformedData = someData.filter((w) => filters.indexOf(w) === -1);

    // If the resulting array is empty, we call the empty action, otherwise we call the success action
    if (transformedData.length === 0) {
      yield put(someActionEmpty());
    } else {
      yield put(someActionSuccess(transformedData));
    }
  } catch (e) {
    // If we got an exception along the way, we call the error action with the error message
    yield put(someActionError(e.message));
  }
}

describe('When testing a complex Saga', () => {
  describe("Scenario 1: When the input contains other words than foo and bar and doesn't throw", () => {
    const it = sagaHelper(mySaga('hello,foo,bar,world'));

    it('should get the list of filters from the state', (result) => {
      expect(result).toEqual(select(selectFilters));

      // Here we specify what the selector should have returned.
      // The selector is not called so we have to give its expected return value.
      return ['foo', 'bar'];
    });

    it('should have called the mock API first, which we are going to specify the results of', (result) => {
      expect(result).toEqual(call(splitApi, 'hello,foo,bar,world'));

      // Here we specify what the API should have returned.
      // Again, the API is not called so we have to give its expected response.
      return ['hello', 'foo', 'bar', 'world'];
    });

    it('and then trigger an action with the transformed data we got from the API', (result) => {
      expect(result).toEqual(put(someActionSuccess(['hello', 'world'])));
    });

    it('and then nothing', (result) => {
      expect(result).toBeUndefined();
    });
  });

  describe('Scenario 2: When the input only contains foo and bar', () => {
    const it = sagaHelper(mySaga('foo,bar'));

    it('should get the list of filters from the state', (result) => {
      expect(result).toEqual(select(selectFilters));
      return ['foo', 'bar'];
    });

    it('should have called the mock API first, which we are going to specify the results of', (result) => {
      expect(result).toEqual(call(splitApi, 'foo,bar'));
      return ['foo', 'bar'];
    });

    it('and then trigger the empty action since foo and bar are filtered out', (result) => {
      expect(result).toEqual(put(someActionEmpty()));
    });

    it('and then nothing', (result) => {
      expect(result).toBeUndefined();
    });
  });

  describe('Scenario 3: The API is broken and throws an exception', () => {
    const it = sagaHelper(mySaga('hello,foo,bar,world'));

    it('should get the list of filters from the state', (result) => {
      expect(result).toEqual(select(selectFilters));
      return ['foo', 'bar'];
    });

    it('should have called the mock API first, which will throw an exception', (result) => {
      expect(result).toEqual(call(splitApi, 'hello,foo,bar,world'));

      // Here we pretend that the API threw an exception.
      // We don't "throw" here but we return an error, which will be considered by the
      // redux-saga-testing helper to be an exception to throw on the generator
      return new Error('Something went wrong');
    });

    it('and then trigger an error action with the error message', (result) => {
      expect(result).toEqual(put(someActionError('Something went wrong')));
    });

    it('and then nothing', (result) => {
      expect(result).toBeUndefined();
    });
  });
});

Other examples

You have other examples in the various tests folders.

FAQ

How can I test a Saga that uses take or takeEvery?

You should separate this generator in two: one that only uses take or takeEvery (the "watchers"), and the ones that atually run the code when the wait is over, like so:

import { takeEvery } from 'redux-saga';
import { put } from 'redux-saga/effects';
import { SOME_ACTION, ANOTHER_ACTION } from './state';

export function* onSomeAction(action) {
        const { payload: data } = action;
        yield put(actionGenerator(data));
}

export function* onAnotherAction() {
        etc.
}

export default function* rootSaga() {
    yield [
        takeEvery(SOME_ACTION, onSomeAction),
        takeEvery(ANOTHER_ACTION, onAnotherAction),
        etc.
    ];
}

From the previous example, you don't have to test rootSaga but you can test onSomeAction and onAnotherAction.

Do I need to mock the store and/or the state?

No you don't. If you read the examples above carefuly, you'll notice that the actual selector (for example) is never called. That means you don't need to mock anything, just return the value your selector should have returned. This library is designed to test a Saga workflow, not testing your actual selectors. If you need to test a selector, do it in isolation (it's just a pure function after all).

Code coverage

This library should be compatible with your favourite code-coverage frameworks.

In the GitHub repo, you'll find examples using Istanbul (for Mocha) and Jest.

Change Log

v2.0.1

  • Updating dependencies
  • Fix AVA's config
  • README wording

v2.0.0

  • Migration to TypeScript
  • Updating dependencies

v1.0.5

  • Updating dependencies
  • Adding examples using selectors (thanks @TAGC for the suggestion)

v1.0.4

  • Updating dependencies
  • Fixed Ava issues with babel-polyfill

v1.0.3

  • Adding documentation regarding take and takeEvery
  • Updating dependencies

v1.0.2

  • Updating dependencies
  • Jest updated from 0.16 to 0.17
  • redux-saga upgraded to the latest 0.13 version

v1.0.1

  • Fixing a Yarn.lock issue
  • Fixing a few readme problems

v1.0.0

  • Adding code-coverage support for Jest and Mocha
  • The API will stay stable, and will enforce semver.

v0.1.1

  • Adding Yarn support
  • Minifying the generated ES3 helper

v0.1.0

  • Adding AVA tests
  • Improved documentation

v0.0.5

  • Bug fix on npm test

v0.0.4

  • Adding Mocha tests
  • Moved Jest tests to their own folders

v0.0.3

  • Adding Travis support
  • Improve documentation

v0.0.2

  • Making the helper ES3 compatible

v0.0.1

  • Basic functionality
  • Addubg Jest tests examples
  • Readme
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].