GithubHelp home page GithubHelp logo

antoinejaussoin / redux-saga-testing Goto Github PK

View Code? Open in Web Editor NEW
156.0 5.0 13.0 903 KB

A no-brainer way of testing your Sagas

License: MIT License

JavaScript 1.33% TypeScript 98.67%
sagas mocha ava redux redux-saga testing jest testing-sagas

redux-saga-testing's Introduction

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(): any {
  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(): any {
  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): any {
  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: any) {
    // 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(): any {
    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.2

  • Updating dependencies
  • Fix TypeScript typings

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

redux-saga-testing's People

Contributors

antoinejaussoin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

redux-saga-testing's Issues

React Native: Couldn't find preset "env" relative to directory

When trying to test our React Native application's saga's I am getting the following error.
Couldn't find preset "env" relative to directory ...node_modules/redux-saga-testing". After looking on the web I found a similar issue timbuckley/redux-saga-test-engine#26.

After looking at that post. I deleted the .babelrc file from the node_modules/redux-saga-testing folder and the test suite passed. I think a simple fix would not be to publish the .babelrc file to NPM. Please let me know if you need anymore information

How to test a race

Hi, first of all thank you for taking the time to create and release this package. Saved a lot of time for me.

Question
How to test a race in redux-saga

Saga code

// Render a simple custom alert view with OK and CANCEL buttons
yield put(SettingsActions.alertView.showAlertView());

// Wait for user to interact with one of the buttons before saga continues
const { alertAction } = yield race({
    alertAction: take(SettingsActionTypes.alertView.ON_ACTION_TRIGGERED),
});

// User interacted with either OK or CANCEL so take appropriate action (save relevant prefs)
switch (alertAction.payload) {
    case SettingsFieldTypes.alertView.type.CONFIRM:
        yield put(RealmActions.saveUser(**with OK setting**));
        break;
    default:
        yield put(RealmActions.saveUser(**with CANCEL setting**));
        break;
}

Saga test

// Test if an alert is getting trigger
it('should call an AlertView', (result) => {
    expect(result).toEqual(put(SettingsActions.alertView.showAlertView()));
});

// Test if a race is getting called?
it('should wait for AlertView action triggered', (result) => {
    expect(result).toEqual(race({
        alertAction: take(SettingsActionTypes.alertView.ON_ACTION_TRIGGERED),
    }));
});

// Up to here it's fine, but after this point how do I test the saga?
// In actuality, we wait for user to tap on OK, or CANCEL before continuing with the saga
// But how do I simulate that

How to return thrown object in test

In my saga I have code that looks like this:

  try {
// removed for brevity
  } catch (e) {
    const errorMsg = e && e.response ? `Error ${e.response.status} - Unable to create Timesheet` : 'An unknown error occurred';
    const errorObj = e && e.response && e.response.data && e.response.data.result || store.timesheet.model
    yield put(timesheet.actions.setTimesheets(store.timesheet.collection, errorObj, errorMsg));
  }

I have this in my saga test:

  it('should call api', (result) => {
    expect(result).toEqual(call(api, mockState.auth, '/timesheets'));
    return new Error('Something went wrong')
  });

which works great for the generic "unknown error occurred" but I need to test throwing an object like:

    throw({
      response: {
        data: { result: mockTimesheetWithError },
        status: 406
      }
    });

But I can't figure out actually throw the object back to the object. Any ideas?

Take.

How can you test a saga that "takes" values from an action's payload? That the only example that I haven't found on the documentation.
Example:

function* mySaga () => {
  const { payload: data } = yield take(actionName);
  yield put(actionGenerator(data));
}

I would like to test if actionGenerator is called and with which params but execution won't even pass to that line because data can't be assigned.

How to test selectors in saga that take action values as props?

Having issues testing a selector that takes other arguments besides state

export function* exampleSaga(action) {
    const selectVar = yield select(state => exampleSelector(state, { id: action.id }))
    ...
}
describe('exampleSaga', () => {
    let it = sagaHelper(exampleSaga(mockAction))
    it('should yield x', (result) => {
        expect(result).toEqual(
            select(state => exampleSelector(state, { id: mockAction.id }))
        )
        return 'x'
    })
})

I've also tried saving the selector as a separate variable:

export const exampleSelectorFn = (action) => (state) => (
    shouldChangeRouteSelector(state, { action: action.id })
)
export function* exampleSaga(action) {
    const selectVar = yield select(exampleSelectorFn(action))
    ...
}
describe('exampleSaga', () => {
    let it = sagaHelper(exampleSaga(mockAction))
    it('should yield x', (result) => {
        expect(result).toEqual(
            select(exampleSelectorFn(mockAction))
        )
        return 'x'
    })
})

Both returns this (using jest):

Expected value to equal:
    {"@@redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function anonymous]}}
Received:
    {"@@redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function anonymous]}}

Difference:

Compared values have no visual difference.

How to inject/mock the store?

Hey @antoinejaussoin !
Great work you have done here.

One question: I have rather complex saga that acts accordingly to the state of my store. I get the state by starting off with yield select() of course.

How can I inject/mock my store if I was to use your library for testing my saga?

Thanks!

How to test array of put's in yield

@antoinejaussoin this library is awesome !!
can you help me with below scenario to write test case where am calling actions in array of yield.

function* testSagas() {
yield[
  put(someAction()),
  put(someAction1()),
  put(someAction2())
]
}

Test case i tried to implement,

describe('Testing saga', () => {
      const it = sagaHelper(testSagas())
      it('Should Trigger someAction', result => {
        expect(result).toEqual(put(someAction()))
      })
      it('Should Trigger someAction1', result => {
        expect(result).toEqual(put(someAction1()))
      })
      it('Should Trigger someAction2', result => {
        expect(result).toEqual(put(someAction1()))
      })
      it('End of Saga', result => {
          expect(result).toBeUndefined()
      })
    })
  })

How can I explicitly detect generator termination? (Feature Request?)

function* saga() {
  yield
}
it = sagaHelper(saga())
it('should be undefined (active)', result => {
   expect(result).toBeUndefined()
})
it('should be undefined (terminated)', result => {
   expect(result).toBeUndefined()
})

saga().next() returns

  1. { value: undefined, done: false }
  2. { value: undefined, done: true }

How can I tell them?

Test `getContext` function from redux-saga

I use the getContext feature in redux-saga to help manage endpoints and imports. Is there any support to write tests for these?

const authApi = yield getContext('authApi') // contains an authEndpoint function that returns the api
const apiHelper = yield getContext('apiHelper')

I think regardless of what I return there, I am getting cant read authEndpoint from undefined

Using beforeEach/beforeAll

I wanted to setup some mocks before actual test cases, but it seems that beforeEach/beforeAll methods are not called at all.
Here's sample of my code:

    describe('elementInfoLoad', () => {

        let authenticatedFetchMock;
        let formSaga;
        beforeEach(() => {
            authenticatedFetchMock = {
                authenticatedFetchAsync: () => {},
            };
            formSaga = formSagaInjector({
                '../../Utils/authenticatedFetch': authenticatedFetchMock,
            });
        })

        describe('should load element data for lite', () => {
            const it = sagaHelper(formSaga.elementInfoLoad());

            it('should dispatch `formElementInfoLoading` action', result => {
                expect(result).toEqual(put(formElementInfoLoading()));
            });

        });
    });

In this case formSaga will always be undefined. It works fine without beforeEach, although it is kinda messy.
Is it possible to do it this way, or am i missing something ?

Support jest coverage

@antoinejaussoin this library is fantastic! Thank you so much!

Have you had success with using jest --coverage when overriding it? It seems to fail for my project.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.