# redux-saga-test-plan offers two testing styles, and the integration one deliberately stops asserting order

> A testing library for Redux Saga that lets you assert exact effects and their ordering, or assert only that a particular put happened at some point in a real saga run. The second style is the interesting one, because it trades precision for resilience to refactoring.

**jfairbank/redux-saga-test-plan** — Test Redux Saga with an easy plan.

- Repository: https://github.com/jfairbank/redux-saga-test-plan
- Website: http://redux-saga-test-plan.jeremyfairbank.com
- Stars: 1,242 · Forks: 128
- Language: JavaScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/jfairbank-redux-saga-test-plan

## The core argument is that effect order is not behaviour

The README states the problem with unit testing sagas in one sentence. Unit testing couples your test to your implementation, and a simple reordering of yielded effects in your saga could break your tests even if the functionality stays the same.

That is the sharpest observation in the project, and it is the reason the library has two modes. If you are not concerned with the order or the exact effects your saga yields, you can take an integrative approach and test the behaviour of your saga when it is run by Redux Saga. You then simply test that a particular effect was yielded during the saga run. That is `expectSaga`.

The alternative is the other mode, where you assert exact effects and their ordering, which is the unit testing style.

The library describes itself as aiming to embrace both integration testing and unit testing approaches, and the phrasing in the summary is that whether you need to test exact effects and their ordering or just that your saga puts a specific action at some point, the library covers you.

One prerequisite is called out for the integration side: it requires a global `Promise` to be available. The reason is that the tests always run asynchronously, since `expectSaga` runs your saga with Redux Saga for real.

The interesting consequence for a team is that the two modes let you be deliberate about coupling. A test that asserts a put happened somewhere is robust to a refactor. A test that asserts the exact sequence is precise and will fail when you reorder, which is correct if the order is the contract and wrong if it is not.

## The chainable API ends in run, and the promise is the whole contract

The simple example shows the shape of the integration API, and the comments in it carry most of the explanation.

```js
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)
    .put({
      type: 'RECEIVE_USER',
      payload: { id: 42, name: 'Tucker' },
    })
    .dispatch({ type: 'REQUEST_USER', payload: 42 })
    .run();
});
```

You pass the saga function to `expectSaga`, and any additional arguments become arguments to the saga, which is why `api` appears twice. The return value is a chainable API with assertions for the different effect creators available in Redux Saga.

`put` asserts that the put will eventually happen. `dispatch` supplies actions to take effects, which is what feeds the `take('REQUEST_USER')` in the saga. `run` starts the test and returns a Promise.

The promise is the contract with the test runner. Tests with `expectSaga` always run asynchronously, so the returned Promise resolves when the saga finishes or when `expectSaga` forces a timeout. The README is explicit that if you use a runner like Jest you should return the Promise inside your Jest test so Jest knows when the test is complete. Forget the return and the test passes vacuously, which is the single most common mistake with this library.

The timeout is the other half of the contract, and it is the mechanism that turns a missing effect into a failure rather than a hang.

## Providers intercept effects before Redux Saga sees them

Because `expectSaga` runs your saga with Redux Saga for real, it resolves effects just like Redux Saga would in your application. The README calls that great for integration testing but notes that sometimes it is laborious to bootstrap your entire application for tests, or to mock things like server APIs.

Providers are the answer. They are described as similar to middleware that allow you to intercept effects before they reach Redux Saga. You can return a mock value instead of allowing Redux Saga to handle the effect, or pass the effect on to other providers or eventually to Redux Saga itself.

```js
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();
```

The provide array is a list of tuple pairs containing a matcher and a fake value. You can use the effect creators from Redux Saga directly, or the matchers from the `redux-saga-test-plan/matchers` module, and the advantage of the library's own matchers is the partial matchers such as `call.fn`, which matches by the function without worrying about the specific args contained in the actual call effect.

The same module supplies `throwError` from `redux-saga-test-plan/providers`, which lets a test simulate a failing call so the saga's error path can be exercised.

```js
return expectSaga(userSaga, api)
  .provide([
    [matchers.call.fn(api.fetchUser), throwError(error)]
  ])
  .put({ type: 'FAIL_USER', error })
  .dispatch({ type: 'REQUEST_USER', payload: 42 })
  .run();
```

There are two flavours of provider. Static providers are easier to compose and reuse, and dynamic providers give more flexibility with non-deterministic effects. The distinction is about whether the mock value is fixed at provide time or computed when the effect fires, which matters when the value depends on the call arguments.

## The package ships matchers and providers as separate entry points

The package layout is the clearest statement of the library's design, and it is a design decision rather than an accident of bundling.

The published `files` array includes `decls`, `src`, `lib`, `matchers.js`, `providers.js` and the type declaration files. The main entry point is `lib/index.js`, the built output of the `src` directory, and the typings point at `index.d.ts`.

The separate top level `matchers.js` and `providers.js` are the reason you import them from `redux-saga-test-plan/matchers` and `redux-saga-test-plan/providers` rather than from the main entry. That is not cosmetic. The matchers and the providers are useful on their own, and pulling them into the main bundle would mean every consumer pays for them whether or not they mock anything.

The type declarations show the same care. There are separate `index.d.ts`, `matchers.d.ts`, `providers.d.ts` and `effects.d.ts` files, plus a `decls/` directory, and Flow declarations are copied into the build by a dedicated script that runs `copyFileSync` on `decls/index.js` to produce `lib/index.js.flow`.

The build is Babel compiling `src` into `lib`, ignoring the mock files. The check script chains lint, typecheck and test, and the typecheck is `flow`, with a `.flowconfig` at the root, so the project type checks with Flow and ships TypeScript declarations separately.

The `book.json` and `docs/` directory indicate that the documentation is built with GitBook, and there is a `docs:publish` script that builds the book, copies the CNAME file into it, creates a git repository, switches to a gh-pages branch, adds a `.nojekyll` file and force pushes. That is the whole documentation pipeline in one script line, and it explains the `CNAME` file at the repository root and the documentation site.

## Release history and the state of a well used library

The maintenance picture is the part to weigh carefully here, and it is different from the projects around it.

The published version in the package metadata is 4.0.6, while the release tags on GitHub are 4.0.0 from 2020-10-16, v4.0.0-rc.3 from 2019-09-26 and v4.0.0-rc.2 from 2019-09-25. The last push to the repository was on 2026-05-09 and the repository is not archived.

So the library is effectively stable at a 4.x line, the tagged releases have not moved since 2020, and the source has seen more recent commits than the last tag suggests. That combination is common for a library that most people depend on through their package manager and that a maintainer still touches occasionally without cutting a new release.

For a Redux Saga testing library, that is a defensible position, because the library's job is to run your saga and observe its effects, and that contract has not needed to change. The risk is different from the usual one. A library that is not releasing is not likely to break, but it is also not going to gain support for a new major version of Redux Saga if one appears with breaking changes.

The licence is MIT, the author is Jeremy Fairbank, and the project is published to npm as `redux-saga-test-plan` with a homepage pointing at the documentation site at redux-saga-test-plan.jeremyfairbank.com.

The keywords are worth a glance because they reveal the intended framing: redux, saga, redux-saga, test, testing, tdd and bdd. The library is positioned as a testing tool rather than as a saga utility, and the topics list adds integration testing and unit testing to that.

The infrastructure is dated in a way that is worth noting if you are contributing rather than consuming. There is a `.travis.yml` for continuous integration, a `.babelrc` and an `.eslintrc.json` for the older lint configuration, a `prettierrc.json` with a `prettierignore`, and the test script is plain `jest` with a `test:ci` variant running in band. The repository also has a `__tests__/` directory at the root for the library's own tests, and a `yarn.lock`.

## Conclusion

Adopt redux-saga-test-plan if you run sagas and want a testing API that lets you choose per test how tightly to couple the test to the implementation. Do not adopt it as a way to avoid writing plain unit tests, because the ordering-sensitive mode is still the right tool when the order of effects is the behaviour you care about. Verify first that the version of redux-saga you use is compatible with the library, that your test runner handles the returned promise from expectSaga, and that the matchers you rely on for partial effect matching behave the way you expect before you rely on them to match loosely.

## FAQ

### What is the difference between expectSaga and the unit testing style in redux-saga-test-plan?

expectSaga runs your saga with Redux Saga for real and lets you assert that a particular effect happened at some point, so it does not break when you reorder yielded effects. The unit testing style asserts exact effects and their ordering, which is more precise but couples the test to the implementation.

### How do I mock a call effect in redux-saga-test-plan?

Use the provide method with an array of matcher and value pairs, for example [call(api.fetchUser, 42), fakeUser]. You can use the matchers from redux-saga-test-plan/matchers, including call.fn which matches by function regardless of arguments, and throwError from the providers module to simulate a failure.

### Why does my expectSaga test need to return the promise?

Tests with expectSaga always run asynchronously, and the returned Promise resolves when the saga finishes or when expectSaga forces a timeout. The README says to return the Promise inside your Jest test so Jest knows when the test is complete.

### Does redux-saga-test-plan support TypeScript and Flow?

The package ships TypeScript declarations in index.d.ts, matchers.d.ts, providers.d.ts and effects.d.ts, and the project type checks with Flow, copying decls/index.js into lib/index.js.flow during the build.

### How current is redux-saga-test-plan?

The package metadata shows version 4.0.6 and the GitHub release tags are 4.0.0 from 2020-10-16 with earlier release candidates in 2019. The last push to the repository was on 2026-05-09 and the repository is not archived.

## Sources

- [jfairbank/redux-saga-test-plan on GitHub](https://github.com/jfairbank/redux-saga-test-plan)
- [License: MIT](https://github.com/jfairbank/redux-saga-test-plan/blob/master/LICENSE)
- [Project website](http://redux-saga-test-plan.jeremyfairbank.com)
- [README](https://github.com/jfairbank/redux-saga-test-plan/blob/master/README.md)
- [Releases](https://github.com/jfairbank/redux-saga-test-plan/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jfairbank-redux-saga-test-plan
