skip to content

Mock Stores & Effect Tests

NgRx code tests in layers: reducers and selector projectors as pure functions, components against a mock store, effects against a mock action stream. Interviewers ask what each layer proves.

on this pageshow

explore

questions

6

In NgRx 22, how do you unit-test a reducer built with createReducer, and what should the test assert about the returned state?

level: juniorimportance: must knowfreq 52%

answer

  1. no store, no providers
  2. a plain function call
  3. an action from its creator
  4. reference equality in both directions
  5. freeze the input you pass

basics

~20 s

An NgRx reducer is a pure function, so call it directly with a start state and an action from its creator, then assert the new values, a new object for handled actions and the same reference for unknown ones.

solid answer

~40 s

A reducer made with `createReducer` is just `(state, action) => state`, so the test needs no `Store`, no `TestBed` and no mocks: import the reducer and its `initialState`, build the action with its action creator, call the reducer and assert. I check three things. For a handled action, the returned state has the expected values **and** is a new object (`not.toBe(before)`), because memoized selectors notice change by reference. For an action no `on()` handles, it returns the very same object (`toBe(state)`). And `reducer(undefined, anyAction)` returns `initialState`. Because the store's immutability runtime check is a meta-reducer that a direct call bypasses, I pass a frozen input (`Object.freeze`, deep for nested state) so a mutating `on()` handler fails the test instead of passing silently.

code

ts · 18 lines
ts
import { createActionGroup, createReducer, emptyProps, on, props } from '@ngrx/store';

export const CouponActions = createActionGroup({
  source: 'Checkout Page',
  events: {
    'Coupon Applied': props<{ code: string; percentOff: number }>(),
    'Coupon Removed': emptyProps(),
  },
});

export interface CouponState { code: string | null; percentOff: number }
export const initialState: CouponState = { code: null, percentOff: 0 };

export const couponsReducer = createReducer(
  initialState,
  on(CouponActions.couponApplied, (state, { code, percentOff }) => ({ ...state, code, percentOff })),
  on(CouponActions.couponRemoved, () => initialState),
);

go deeper

for a junior

Recall that an NgRx reducer is a pure function: call it with a state and an action from the action creator, and assert on what it returns.

for a middle

Explain why both reference assertions matter: a new object for handled actions and the same object for unhandled ones, because selectors memoize by reference.

for a senior

Show that direct calls bypass the store's immutability meta-reducer, and make mutation fail the test with frozen inputs; cover every on() branch, including edge cases.

for a principal

Argue for layered NgRx tests: cheap, exhaustive reducer tests, with component and effect tests that mock the store instead of re-proving the state rules.

## What a reducer is in NgRx In NgRx a **reducer** is the function that computes the next state of one slice from the current state and an **action** (a plain object with a `type` string and optional payload). `createReducer(initialState, ...ons)` builds it: each `on(actionCreator, handler)` maps one or more action types to a handler that returns the new state. Reading the source at NgRx 22, the function `createReducer` returns is tiny: it looks up `action.type` in a map, runs the matching handler, and otherwise returns the state it was given. Its first parameter defaults to `initialState`, so a call with `undefined` state starts the slice. That makes the reducer the easiest layer of an NgRx app to test. It has no dependencies, no injection and no asynchrony, so the test is a plain function call. ## The shape of the test For a checkout feature, take a coupons slice with `code` and `percentOff`, and two actions made with `createActionGroup`: `couponApplied` and `couponRemoved`. The test: 1. Imports the reducer and its exported `initialState`. 2. Builds the action with the **action creator**, for example `CouponActions.couponApplied({ code: 'SAVE10', percentOff: 10 })`, never a hand-written literal, so the `type` string (`'[Checkout Page] Coupon Applied'`) cannot drift from the real one. 3. Calls `couponsReducer(state, action)` and asserts on the result. No `TestBed`, no `provideStore`, no `MockStore`. Those belong to component and effect tests. ## The assertions that matter | Case | Assertion | Why it matters | |---|---|---| | Handled action | `toEqual(expected)` on the values | the business rule is right | | Handled action | `not.toBe(before)` | a new object signals the change to selectors | | Unhandled action | `toBe(state)` | no needless new reference | | `undefined` state | `toBe(initialState)` | the slice boots correctly | The reference checks are not pedantry. **Memoized selectors** built with `createSelector` compare their inputs by reference. If a handler mutates the old object and returns it, the reference does not change, selectors return their cached result and the view shows stale data. If a reducer returns a fresh copy for actions it does not handle, every dependent selector recomputes on every action anywhere in the app. ## Catching mutation in a direct call NgRx has **runtime checks**, and in development mode `strictStateImmutability` defaults to `true`: the store freezes state so a mutation throws. But those checks are implemented as **meta-reducers** that the `Store` wraps around your reducers. When a test calls the reducer function directly, no store exists and nothing is frozen, so a handler that does `state.code = code; return state;` can pass a naive `toEqual` test. The fix is to freeze the input yourself: - `Object.freeze(before)` for a flat slice. Module code runs in strict mode, so writing to a frozen object throws a `TypeError`. - A small recursive deep-freeze helper for nested slices, because `Object.freeze` is shallow. - Pair it with `not.toBe(before)`, which also fails when the handler returns the old object. NgRx's `on()` handlers do not copy state for you; unlike some other libraries, there is no draft or proxy layer, so returning a new object with the spread operator is your job, and the test is where you prove it. ## What this layer does and does not prove A reducer test proves the **state transition**: given this state and this action, this is the next state. It says nothing about: - whether a component dispatches that action (a component test with a mock store proves that); - whether the selectors read the new state correctly (selector tests prove that); - whether the action is ever produced by an effect (an effect test proves that). That split is the point of testing NgRx code in layers: each test is small and fails for one reason. Reducer tests are the cheapest of them, so they can afford to cover every `on()` branch, including edge cases such as applying a second coupon or removing a coupon when none is set. ## Common mistakes in reducer tests - **Hand-written action literals.** `{ type: 'couponApplied' }` matches no handler, because the real type is `'[Checkout Page] Coupon Applied'`; the test then asserts the unchanged state and may pass for the wrong reason. Always build actions with their creators. - **Sharing one mutable fixture.** If several tests reuse the same state object and a handler mutates it, later tests start from corrupted data. Freeze fixtures, or build a fresh one per test. - **Only testing the happy path.** Removing a coupon when none is applied, or applying a second one, are the branches that break in production. - **Reaching for `TestBed`.** Nothing in a reducer needs injection; adding it only slows the suite and hides the fact that the function is pure.

  • Why assert toBe for an action the NgRx reducer does not handle, rather than toEqual?
    `createReducer`'s function returns the incoming state unchanged when no `on()` matches the action type. `toBe` proves the reference is untouched; `toEqual` would also pass for a needless copy. A copy matters because memoized selectors compare inputs by reference, so a fresh object for every unrelated action would make dependent selectors recompute across the app.
  • Your NgRx reducer test passes, yet in the running app the view does not update after couponApplied. What could the test have missed?
    Most likely the handler mutates the old state and returns it. A `toEqual` assertion on an unfrozen input cannot see that, because the mutated object has the right values. In the app the reference did not change, so selectors served cached results. Freezing the input and asserting `not.toBe(before)` would have failed the test.
  • Do NgRx runtime checks help in a reducer unit test?
    Not in a direct call. `strictStateImmutability` is on by default in development, but it runs as a meta-reducer the `Store` wraps around registered reducers. Calling `couponsReducer(state, action)` involves no store, so nothing freezes the state; the test has to freeze its own input.

saying these in an interview costs you the question

  • You need TestBed and provideStore before you can test an NgRx reducer.
  • toEqual on the result is enough; reference identity does not matter in reducers.
  • The store's immutability runtime check also catches mutation in a direct reducer call.
  • NgRx on() handlers get a draft copy, so mutating state inside them is safe.
  • An NgRx reducer should reset the slice to initialState for actions it does not handle.
open as a page

In NgRx, how do you test a component that shows the cart total from the store, using provideMockStore instead of real reducers?

level: middleimportance: must knowfreq 47%

basics

~10 s

Provide provideMockStore from @ngrx/store/testing, pin the cart-total selector with overrideSelector or the selectors option, and assert the rendered total; since no reducers run, verify clicks by spying on store.dispatch.

open as a page

In NgRx, how do you test a createSelector selector through its projector, and what does a projector test leave unproven?

level: middleimportance: should knowfreq 38%

basics

~10 s

Call selector.projector(...) with plain input values to test the projection logic alone; it never runs the input selectors, so a whole-state call is still needed to prove the selector reads the right slices.

open as a page

In NgRx, how do you unit-test a payment effect with provideMockActions, and prove that a declined charge does not kill the effect?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Provide provideMockActions(() => actions$) and a fake payment API, subscribe to the effect and assert the actions it emits; feed a failing charge then a good one and expect a failure action, then a success action.

open as a page

An NgRx MockStore test calls setResult on an overridden cart-total selector, but the view keeps the old total; what is missing, and why reset selectors afterwards?

level: seniorimportance: should knowfreq 30%

basics

~10 s

setResult changes what the selector returns but emits nothing; call store.refreshState() so subscribers re-evaluate. Overrides live on the shared selector objects, so call store.resetSelectors() in afterEach or they leak into later tests.

open as a page

With an NgRx SignalStore whose state is protected, how does a unit test set the store's state directly, and when should it?

level: seniorimportance: nice to knowfreq 22%

basics

~10 s

Wrap the store in unprotected() from @ngrx/signals/testing and call patchState on it; use that only to arrange state no public method can reach, and prefer driving the store through its own methods.

open as a page