redux-saga-testing
Write tests for Redux Sagas using redux-saga-test-plan, runSaga, and manual generator testing. Covers expectSaga (integration), testSaga (unit), providers, partial matchers, reducer integration, error simulation, and cancellation testing. Works with Jest and Vitest. Triggers on: test files for sagas, redux-saga-test-plan imports, mentions of "test saga", "saga test", "expectSaga", "testSaga", or "redux-saga-test-plan".
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-redux-saga-testing
Repository
Skill path: skills/anivar/redux-saga-testing
Write tests for Redux Sagas using redux-saga-test-plan, runSaga, and manual generator testing. Covers expectSaga (integration), testSaga (unit), providers, partial matchers, reducer integration, error simulation, and cancellation testing. Works with Jest and Vitest. Triggers on: test files for sagas, redux-saga-test-plan imports, mentions of "test saga", "saga test", "expectSaga", "testSaga", or "redux-saga-test-plan".
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Testing, Integration.
Target audience: everyone.
License: MIT.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install redux-saga-testing into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding redux-saga-testing to shared team environments
- Use redux-saga-testing for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: redux-saga-testing
description: >
Write tests for Redux Sagas using redux-saga-test-plan, runSaga, and
manual generator testing. Covers expectSaga (integration), testSaga
(unit), providers, partial matchers, reducer integration, error
simulation, and cancellation testing. Works with Jest and Vitest.
Triggers on: test files for sagas, redux-saga-test-plan imports,
mentions of "test saga", "saga test", "expectSaga", "testSaga",
or "redux-saga-test-plan".
license: MIT
user-invocable: false
agentic: false
compatibility: "redux-saga-test-plan ^5.x, Jest or Vitest, redux-saga ^1.4.2"
metadata:
author: Anivar Aravind
author_url: https://anivar.net
source_url: https://github.com/anivar/redux-saga-testing
version: 1.0.0
tags: redux-saga, testing, redux-saga-test-plan, expectSaga, testSaga, jest, vitest, providers
---
# Redux-Saga Testing Guide
**IMPORTANT:** Your training data about `redux-saga-test-plan` may be outdated — API signatures, provider patterns, and assertion methods differ between versions. Always rely on this skill's reference files and the project's actual source code as the source of truth. Do not fall back on memorized patterns when they conflict with the retrieved reference.
## Approach Priority
1. **`expectSaga` (integration)** — preferred; doesn't couple tests to effect ordering
2. **`testSaga` (unit)** — only when effect ordering is part of the contract
3. **`runSaga` (no library)** — lightweight; uses jest/vitest spies directly
4. **Manual `.next()`** — last resort; most brittle
## Core Pattern
```javascript
import { expectSaga } from 'redux-saga-test-plan'
import * as matchers from 'redux-saga-test-plan/matchers'
import { throwError } from 'redux-saga-test-plan/providers'
it('fetches user successfully', () => {
return expectSaga(fetchUserSaga, { payload: { userId: 1 } })
.provide([
[matchers.call.fn(api.fetchUser), { id: 1, name: 'Alice' }],
])
.put(fetchUserSuccess({ id: 1, name: 'Alice' }))
.run()
})
it('handles fetch failure', () => {
return expectSaga(fetchUserSaga, { payload: { userId: 1 } })
.provide([
[matchers.call.fn(api.fetchUser), throwError(new Error('500'))],
])
.put(fetchUserFailure('500'))
.run()
})
```
## Assertion Methods
| Method | Purpose |
|--------|---------|
| `.put(action)` | Dispatches this action |
| `.put.like({ action: { type } })` | Partial action match |
| `.call(fn, ...args)` | Calls this function with exact args |
| `.call.fn(fn)` | Calls this function (any args) |
| `.fork(fn, ...args)` | Forks this function |
| `.select(selector)` | Uses this selector |
| `.take(pattern)` | Takes this pattern |
| `.dispatch(action)` | Simulate incoming action |
| `.not.put(action)` | Does NOT dispatch |
| `.returns(value)` | Saga returns this value |
| `.run()` | Execute (returns Promise) |
| `.run({ timeout })` | Execute with custom timeout |
| `.silentRun()` | Execute, suppress timeout warnings |
## Provider Types
### Static Providers (Preferred)
```javascript
.provide([
[matchers.call.fn(api.fetchUser), mockUser], // match by function
[call(api.fetchUser, 1), mockUser], // match by function + exact args
[matchers.select.selector(getToken), 'mock-token'], // mock selector
[matchers.call.fn(api.save), throwError(error)], // simulate error
])
```
### Dynamic Providers
```javascript
.provide({
call(effect, next) {
if (effect.fn === api.fetchUser) return mockUser
return next() // pass through
},
select({ selector }, next) {
if (selector === getToken) return 'mock-token'
return next()
},
})
```
## Rules
1. **Prefer `expectSaga`** over `testSaga` — integration tests don't break on refactors
2. **Use `matchers.call.fn()`** for partial matching — don't couple to exact args unless necessary
3. **Use `throwError()`** from providers — not `throw new Error()` in the provider
4. **Test with reducer** using `.withReducer()` + `.hasFinalState()` to verify state
5. **Dispatch actions** with `.dispatch()` to simulate user flows in tests
6. **Return the promise** (Jest) or `await` it (Vitest) — don't forget async
7. **Use `.not.put()`** to assert actions are NOT dispatched (negative tests)
8. **Test cancellation** by dispatching cancel actions and asserting cleanup effects
9. **Use `.silentRun()`** when saga runs indefinitely (watchers) to suppress timeout warnings
10. **Don't test implementation** — test behavior (what actions are dispatched, what state results)
## Anti-Patterns
See [references/anti-patterns.md](references/anti-patterns.md) for BAD/GOOD examples of:
- Step-by-step tests that break on reorder
- Missing providers (real API calls in tests)
- Testing effect order instead of behavior
- Forgetting async (Jest/Vitest)
- Inline mocking instead of providers
- Not testing error paths
- Not testing cancellation cleanup
## References
- [API Reference](references/api-reference.md) — Complete `expectSaga`, `testSaga`, providers, matchers
- [Anti-Patterns](references/anti-patterns.md) — Common testing mistakes to avoid
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/anti-patterns.md
```markdown
# Redux-Saga Testing Anti-Patterns
## Table of Contents
- Step-by-step tests that break on reorder
- Missing providers
- Testing implementation instead of behavior
- Forgetting async
- Not testing error paths
- Not testing cancellation
- Inline mocking instead of providers
- Wrong assertion methods
- Over-mocking with dynamic providers
- Not using partial matchers
- Missing negative assertions
- Not testing reducer integration
## Step-by-step tests that break on reorder
```javascript
// BAD: breaks if you swap the order of put and select
it('fetches user', () => {
const gen = fetchUserSaga(action)
expect(gen.next().value).toEqual(select(getToken))
expect(gen.next('token').value).toEqual(call(api.fetchUser, 1))
expect(gen.next(user).value).toEqual(put(fetchUserSuccess(user)))
})
// GOOD: integration test — order doesn't matter
it('fetches user', () => {
return expectSaga(fetchUserSaga, action)
.provide([
[matchers.select.selector(getToken), 'token'],
[matchers.call.fn(api.fetchUser), user],
])
.put(fetchUserSuccess(user))
.run()
})
```
## Missing providers
```javascript
// BAD: makes real API calls — slow, flaky, environment-dependent
it('fetches data', () => {
return expectSaga(fetchDataSaga)
.put(fetchSuccess(data))
.run()
})
// GOOD: mock all external calls
it('fetches data', () => {
return expectSaga(fetchDataSaga)
.provide([
[matchers.call.fn(api.fetchData), mockData],
])
.put(fetchSuccess(mockData))
.run()
})
```
## Testing implementation instead of behavior
```javascript
// BAD: testing that specific effects are yielded in order
testSaga(saga)
.next()
.select(getToken)
.next('token')
.put(setLoading(true))
.next()
.call(api.fetch)
// ... 10 more assertions for internal details
// GOOD: test what the saga DOES, not HOW
expectSaga(saga)
.provide(providers)
.put(fetchSuccess(data)) // observable outcome
.not.put(fetchFailure()) // didn't fail
.run()
```
## Forgetting async
```javascript
// BAD: Jest — test passes before saga finishes
it('fetches data', () => {
expectSaga(saga).provide(providers).put(action).run()
// Missing return! Jest doesn't wait for the promise
})
// GOOD: Jest — return the promise
it('fetches data', () => {
return expectSaga(saga).provide(providers).put(action).run()
})
// GOOD: Vitest — await
it('fetches data', async () => {
await expectSaga(saga).provide(providers).put(action).run()
})
```
## Not testing error paths
```javascript
// BAD: only tests happy path
it('fetches user', () => {
return expectSaga(fetchUserSaga, action)
.provide([[matchers.call.fn(api.fetchUser), user]])
.put(fetchUserSuccess(user))
.run()
})
// GOOD: test both paths
it('fetches user successfully', () => {
return expectSaga(fetchUserSaga, action)
.provide([[matchers.call.fn(api.fetchUser), user]])
.put(fetchUserSuccess(user))
.not.put.actionType('FETCH_USER_FAILURE')
.run()
})
it('handles fetch failure', () => {
return expectSaga(fetchUserSaga, action)
.provide([[matchers.call.fn(api.fetchUser), throwError(new Error('500'))]])
.put(fetchUserFailure('500'))
.not.put.actionType('FETCH_USER_SUCCESS')
.run()
})
```
## Not testing cancellation
```javascript
// BAD: doesn't verify cleanup on cancel
it('syncs data', () => {
return expectSaga(syncSaga)
.provide([[matchers.call.fn(api.sync), data]])
.put(syncSuccess(data))
.run()
})
// GOOD: verify cancellation cleanup
it('cleans up when cancelled', () => {
return expectSaga(syncSaga)
.provide([[matchers.call.fn(api.sync), data]])
.dispatch({ type: 'START_SYNC' })
.dispatch({ type: 'STOP_SYNC' })
.put(syncStopped())
.silentRun()
})
```
## Inline mocking instead of providers
```javascript
// BAD: using jest.mock for saga dependencies
jest.mock('../api', () => ({
fetchUser: jest.fn().mockResolvedValue({ id: 1 }),
}))
it('fetches user', () => {
return expectSaga(fetchUserSaga, action)
.put(fetchUserSuccess({ id: 1 }))
.run()
})
// GOOD: use providers — no global mock pollution
it('fetches user', () => {
return expectSaga(fetchUserSaga, action)
.provide([[matchers.call.fn(api.fetchUser), { id: 1 }]])
.put(fetchUserSuccess({ id: 1 }))
.run()
})
```
## Wrong assertion methods
```javascript
// BAD: using jest expect on generator values manually with expectSaga
it('fetches user', async () => {
const result = await expectSaga(fetchUserSaga, action)
.provide(providers)
.run()
expect(result.effects.put).toContainEqual(put(fetchUserSuccess(user)))
})
// GOOD: use built-in assertions
it('fetches user', () => {
return expectSaga(fetchUserSaga, action)
.provide(providers)
.put(fetchUserSuccess(user))
.run()
})
```
## Over-mocking with dynamic providers
```javascript
// BAD: dynamic provider intercepts everything — hides bugs
.provide({
call() { return 'mocked' }, // ALL calls return 'mocked'
select() { return {} }, // ALL selects return empty object
})
// GOOD: mock only what you need, let the rest pass through
.provide({
call(effect, next) {
if (effect.fn === api.fetchUser) return mockUser
return next() // other calls execute normally or hit other providers
},
})
```
## Not using partial matchers
```javascript
// BAD: breaks when API args change (e.g., adding a header param)
.provide([
[call(api.fetchUser, 1, { headers: { auth: 'token' } }), user],
])
// GOOD: match by function only — resilient to arg changes
.provide([
[matchers.call.fn(api.fetchUser), user],
])
```
## Missing negative assertions
```javascript
// BAD: only checks what happens — doesn't verify what SHOULDN'T happen
it('successful login', () => {
return expectSaga(loginSaga, credentials)
.provide(providers)
.put(loginSuccess(token))
.run()
})
// GOOD: verify both positive and negative outcomes
it('successful login', () => {
return expectSaga(loginSaga, credentials)
.provide(providers)
.put(loginSuccess(token))
.not.put.actionType('LOGIN_FAILURE')
.not.put.actionType('LOGIN_ERROR')
.run()
})
```
## Not testing reducer integration
```javascript
// BAD: tests dispatched actions but not resulting state
it('loads users', () => {
return expectSaga(loadUsersSaga)
.provide(providers)
.put(usersLoaded(users))
.run()
})
// GOOD: verify final state through reducer
it('loads users into state', () => {
return expectSaga(loadUsersSaga)
.withReducer(usersReducer)
.withState({ users: [], loading: true })
.provide(providers)
.hasFinalState({ users: mockUsers, loading: false })
.run()
})
```
```
### references/api-reference.md
```markdown
# Redux-Saga Testing API Reference
## Table of Contents
- Setup (Jest / Vitest)
- expectSaga (Integration Testing)
- testSaga (Unit Testing)
- Providers (Static / Dynamic)
- Matchers
- Reducer Integration
- Dispatching Actions
- Testing Patterns (Cancellation, Race, Throttle)
- runSaga (No Library)
- Manual Generator Testing
---
## Setup
### Installation
```bash
npm install --save-dev redux-saga-test-plan
```
### Jest
```javascript
// Return the promise
it('works', () => {
return expectSaga(mySaga).run()
})
```
### Vitest
```javascript
// async/await
it('works', async () => {
await expectSaga(mySaga).run()
})
```
Both runners work identically — `expectSaga.run()` returns a Promise.
---
## expectSaga — Integration Testing
### Basic Usage
```javascript
import { expectSaga } from 'redux-saga-test-plan'
import * as matchers from 'redux-saga-test-plan/matchers'
import { throwError } from 'redux-saga-test-plan/providers'
it('fetches user', () => {
const user = { id: 1, name: 'Alice' }
return expectSaga(fetchUserSaga, { payload: { userId: 1 } })
.provide([
[matchers.call.fn(api.fetchUser), user],
])
.put(fetchUserSuccess(user))
.run()
})
```
### Assertion Methods
```javascript
expectSaga(saga, ...args)
// Effect assertions
.put(action) // saga dispatches this action
.put.like({ action: { type } }) // partial match on action
.put.actionType('TYPE') // match action type only
.call(fn, ...args) // saga calls fn with exact args
.call.fn(fn) // saga calls fn (any args)
.call.like({ fn, args }) // partial match
.fork(fn, ...args) // saga forks fn
.fork.fn(fn) // saga forks fn (any args)
.spawn(fn, ...args) // saga spawns fn
.select(selector, ...args) // saga selects with selector
.take(pattern) // saga takes pattern
.race(effects) // saga races effects
.all(effects) // saga runs all effects
// Negative assertions
.not.put(action) // does NOT dispatch
.not.call.fn(fn) // does NOT call fn
.not.fork.fn(fn) // does NOT fork fn
// Return value
.returns(value) // saga returns this value
// Action simulation
.dispatch(action) // simulate action during test
// Reducer integration
.withReducer(reducer) // hook up reducer
.withState(initialState) // set initial state
.hasFinalState(expectedState) // assert final state
// Execution
.run() // run saga (returns Promise)
.run({ timeout: 500 }) // with custom timeout (default: 250ms)
.silentRun() // suppress timeout warnings
.silentRun(500) // suppress + custom timeout
```
### Run Options
```javascript
const { effects, storeState, returnValue } = await expectSaga(saga)
.provide(providers)
.run()
// effects contains all yielded effects for inspection
// storeState is the final state (if withReducer used)
// returnValue is the saga's return value
```
---
## testSaga — Unit Testing
Tests exact effect ordering. Use when order is part of the contract.
```javascript
import { testSaga } from 'redux-saga-test-plan'
it('processes in order', () => {
testSaga(checkoutSaga, action)
.next() // start generator
.put(showLoading()) // first yield must be this put
.next()
.call(api.validateCart) // second must be this call
.next({ valid: true }) // feed result
.call(api.processPayment) // third
.next({ id: 'txn_123' })
.put(checkoutSuccess('txn_123')) // fourth
.next()
.isDone() // generator is done
})
```
### Branching with Clone
```javascript
it('handles both branches', () => {
const saga = testSaga(mySaga, action)
.next()
.call(api.validate)
const validBranch = saga.clone()
const invalidBranch = saga.clone()
validBranch.next({ valid: true }).put(success())
invalidBranch.next({ valid: false }).put(failure())
})
```
### Error Testing
```javascript
testSaga(mySaga, action)
.next()
.call(api.fetch)
.throw(new Error('Network error')) // simulate thrown error
.put(fetchFailed('Network error'))
.next()
.isDone()
```
### Save and Restore
```javascript
testSaga(mySaga)
.next()
.take('ACTION')
.save('before branch') // save point
.next(actionA)
.put(resultA())
.restore('before branch') // go back
.next(actionB)
.put(resultB())
```
---
## Providers
### Static Providers (Array of Tuples)
```javascript
.provide([
// Exact effect match
[call(api.fetchUser, 1), { id: 1, name: 'Alice' }],
// Partial match by function (recommended — ignores args)
[matchers.call.fn(api.fetchUser), { id: 1, name: 'Alice' }],
// Mock selector
[matchers.select.selector(getAuthToken), 'mock-token'],
// Simulate error
[matchers.call.fn(api.save), throwError(new Error('500'))],
// Mock select without selector (full state)
[matchers.select(), { auth: { token: 'abc' } }],
])
```
### Dynamic Providers (Object)
```javascript
.provide({
call(effect, next) {
if (effect.fn === api.fetchUser) {
return { id: 1, name: 'Alice' }
}
// Pass through to other providers or real execution
return next()
},
select({ selector }, next) {
if (selector === getAuthToken) return 'mock-token'
return next()
},
fork(effect, next) {
if (effect.fn === backgroundSync) return createMockTask()
return next()
},
})
```
### Composing Providers
```javascript
import { combineProviders } from 'redux-saga-test-plan/providers'
const authProviders = [
[matchers.select.selector(getToken), 'mock-token'],
[matchers.call.fn(api.refreshToken), 'new-token'],
]
const dataProviders = [
[matchers.call.fn(api.fetchUsers), mockUsers],
]
expectSaga(saga)
.provide([...authProviders, ...dataProviders])
.run()
```
---
## Matchers
```javascript
import * as matchers from 'redux-saga-test-plan/matchers'
matchers.call(fn, ...args) // exact match
matchers.call.fn(fn) // function only (any args)
matchers.call.like({ fn, args }) // partial match
matchers.fork(fn, ...args)
matchers.fork.fn(fn)
matchers.spawn(fn, ...args)
matchers.spawn.fn(fn)
matchers.select() // any select
matchers.select.selector(sel) // specific selector
matchers.put(action) // exact action
matchers.put.like({ action }) // partial action match
matchers.put.actionType(type) // type only
matchers.take(pattern) // specific pattern
```
---
## Reducer Integration
```javascript
import usersReducer from './usersSlice'
it('loads users into state', () => {
return expectSaga(loadUsersSaga)
.withReducer(usersReducer)
.withState({ users: [], loading: false, error: null })
.provide([
[matchers.call.fn(api.fetchUsers), [{ id: 1, name: 'Alice' }]],
])
.hasFinalState({
users: [{ id: 1, name: 'Alice' }],
loading: false,
error: null,
})
.run()
})
```
### Combined Reducers
```javascript
import { combineReducers } from '@reduxjs/toolkit'
const rootReducer = combineReducers({ users: usersReducer, auth: authReducer })
expectSaga(saga)
.withReducer(rootReducer)
.withState({ users: initialUsersState, auth: initialAuthState })
.hasFinalState(expectedState)
.run()
```
---
## Dispatching Actions During Tests
Simulate user flows by dispatching actions:
```javascript
it('handles login then logout', () => {
return expectSaga(loginFlowSaga)
.provide([
[matchers.call.fn(api.login), { token: 'abc' }],
])
.dispatch({ type: 'LOGIN', payload: credentials })
.dispatch({ type: 'LOGOUT' })
.put(loginSuccess({ token: 'abc' }))
.put(loggedOut())
.run({ timeout: 500 })
})
```
---
## Testing Patterns
### Test Cancellation
```javascript
it('cleans up on cancel', () => {
return expectSaga(syncSaga)
.provide([
[matchers.call.fn(api.sync), 'data'],
])
.dispatch({ type: 'START_SYNC' })
.dispatch({ type: 'STOP_SYNC' })
.put(syncCancelled())
.silentRun()
})
```
### Test Race / Timeout
```javascript
it('handles timeout', () => {
return expectSaga(fetchWithTimeoutSaga)
.provide([
// Make API call take forever by not providing a value
// delay will win the race
[matchers.call.fn(api.fetch), new Promise(() => {})],
])
.put(timeoutError())
.run({ timeout: 10000 })
})
```
### Test with createMockTask
```javascript
import { createMockTask } from '@redux-saga/testing-utils'
it('cancels task on logout', () => {
const mockTask = createMockTask()
return testSaga(loginFlowSaga)
.next()
.take('LOGIN')
.next({ type: 'LOGIN', payload: creds })
.fork(authorizeSaga, creds)
.next(mockTask)
.take(['LOGOUT', 'LOGIN_ERROR'])
.next({ type: 'LOGOUT' })
.cancel(mockTask)
})
```
### Test Throttle / Debounce
```javascript
it('debounces search', () => {
return expectSaga(watchSearchSaga)
.provide([[matchers.call.fn(api.search), results]])
.dispatch({ type: 'SEARCH', query: 'a' })
.dispatch({ type: 'SEARCH', query: 'ab' })
.dispatch({ type: 'SEARCH', query: 'abc' })
.put(searchResults(results))
.silentRun(1000)
})
```
### Test Saga with Channel
```javascript
it('processes from channel', () => {
return expectSaga(watchRequestsSaga)
.provide([
[matchers.call.fn(handleRequest), 'processed'],
])
.dispatch({ type: 'REQUEST', payload: { id: 1 } })
.dispatch({ type: 'REQUEST', payload: { id: 2 } })
.call.fn(handleRequest)
.silentRun()
})
```
---
## runSaga — No Library Needed
```javascript
import { runSaga } from 'redux-saga'
async function recordSaga(saga, initialAction, state = {}) {
const dispatched = []
await runSaga(
{
dispatch: (action) => dispatched.push(action),
getState: () => state,
},
saga,
initialAction,
).toPromise()
return dispatched
}
// Jest
it('dispatches success', async () => {
jest.spyOn(api, 'fetchUser').mockResolvedValue({ id: 1 })
const dispatched = await recordSaga(fetchUserSaga, fetchUser(1))
expect(dispatched).toContainEqual(fetchUserSuccess({ id: 1 }))
})
// Vitest
it('dispatches success', async () => {
vi.spyOn(api, 'fetchUser').mockResolvedValue({ id: 1 })
const dispatched = await recordSaga(fetchUserSaga, fetchUser(1))
expect(dispatched).toContainEqual(fetchUserSuccess({ id: 1 }))
})
```
---
## Manual Generator Testing
Last resort — most brittle, breaks on reorder:
```javascript
it('yields correct effects', () => {
const gen = fetchUserSaga({ payload: { userId: 1 } })
// Step through
expect(gen.next().value).toEqual(call(api.fetchUser, 1))
// Feed result
const user = { id: 1, name: 'Alice' }
expect(gen.next(user).value).toEqual(put(fetchUserSuccess(user)))
expect(gen.next().done).toBe(true)
})
// Test error path
it('handles errors', () => {
const gen = fetchUserSaga({ payload: { userId: 1 } })
gen.next() // advance to call
expect(gen.throw(new Error('404')).value).toEqual(
put(fetchUserFailure('404'))
)
})
```
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### README.md
```markdown
# Redux-Saga Testing
Created by **[Anivar Aravind](https://anivar.net)**
An AI agent skill for writing tests for Redux Sagas using redux-saga-test-plan. Works with Jest and Vitest.
## The Problem
AI agents often write saga tests that are brittle (asserting exact effect order when it doesn't matter), miss provider setup (causing tests to hit real APIs), or use manual generator stepping when `expectSaga` would be simpler and more maintainable. The result: tests that break on harmless refactors and pass when they shouldn't.
## This Solution
A focused testing skill covering `expectSaga` (integration), `testSaga` (unit), providers, matchers, and reducer integration — with 12 anti-patterns showing exactly what goes wrong and how to fix it.
## Install
```bash
npx skills add anivar/redux-saga-testing -g
```
Or with full URL:
```bash
npx skills add https://github.com/anivar/redux-saga-testing
```
## Baseline
- redux-saga-test-plan ^5.x
- redux-saga ^1.4.2
- Jest or Vitest
## What's Inside
### Testing Approaches
| Approach | Type | Use When |
|----------|------|----------|
| `expectSaga` | Integration | Default choice — order-independent, async, assertions on effects and state |
| `testSaga` | Unit | Effect ordering matters (e.g., must `select` before `call`) |
| `runSaga` | Manual | Need full store integration or custom middleware |
### Providers
| Provider Type | Purpose |
|---------------|---------|
| Static providers | Map effect → return value (simple, covers most cases) |
| Dynamic providers | Custom logic per effect type (conditional mocking) |
| Partial matchers | `matchers.call.fn(apiFn)` — match by function regardless of args |
### Anti-Patterns
12 common testing mistakes with BAD/GOOD code examples:
- Asserting effect order in integration tests
- Missing providers for API calls
- Not awaiting `expectSaga` (test always passes)
- Testing implementation instead of behavior
- Ignoring error paths and cancellation
## Structure
```
├── SKILL.md # Entry point for AI agents
└── references/
├── api-reference.md # Complete expectSaga, testSaga, providers, matchers API
└── anti-patterns.md # 12 common testing mistakes to avoid
```
## Ecosystem — Skills by [Anivar Aravind](https://anivar.net)
### Testing Skills
| Skill | What it covers | Install |
|-------|---------------|---------|
| [jest-skill](https://github.com/anivar/jest-skill) | Jest best practices — mock design, async testing, matchers, timers, snapshots | `npx skills add anivar/jest-skill -g` |
| [zod-testing](https://github.com/anivar/zod-testing) | Zod schema testing — safeParse, mock data, property-based | `npx skills add anivar/zod-testing -g` |
| [msw-skill](https://github.com/anivar/msw-skill) | MSW 2.0 API mocking — handlers, responses, GraphQL | `npx skills add anivar/msw-skill -g` |
### Library & Framework Skills
| Skill | What it covers | Install |
|-------|---------------|---------|
| [zod-skill](https://github.com/anivar/zod-skill) | Zod v4 schema validation, parsing, error handling | `npx skills add anivar/zod-skill -g` |
| [redux-saga-skill](https://github.com/anivar/redux-saga-skill) | Redux-Saga effects, fork model, channels, RTK | `npx skills add anivar/redux-saga-skill -g` |
| [msw-skill](https://github.com/anivar/msw-skill) | MSW 2.0 handlers, responses, migration | `npx skills add anivar/msw-skill -g` |
### Engineering Analysis
| Skill | What it covers | Install |
|-------|---------------|---------|
| [contributor-codebase-analyzer](https://github.com/anivar/contributor-codebase-analyzer) | Code analysis, annual reviews, promotion readiness | `npx skills add anivar/contributor-codebase-analyzer -g` |
## Author
**[Anivar Aravind](https://anivar.net)** — Building AI agent skills for modern JavaScript/TypeScript development.
## License
MIT
```
### _meta.json
```json
{
"owner": "anivar",
"slug": "redux-saga-testing",
"displayName": "Redux Saga Testing",
"latest": {
"version": "1.0.1",
"publishedAt": 1772684707568,
"commit": "https://github.com/openclaw/skills/commit/95642e65fce143e8ffaf350f74bdb6f80e317f20"
},
"history": []
}
```