← Back to Chapters

NgRx Testing

? NgRx Testing

Verify that your NgRx actions, reducers, selectors, and effects behave predictably and safely as your app grows.

? Quick Overview

NgRx testing focuses on unit testing the individual building blocks of state management: Actions, Reducers, Selectors, and Effects. Each piece is tested in isolation so that your global store remains predictable, debuggable, and easy to refactor.

? Key Concepts

  • ? Actions – Plain objects that describe what happened in your app.
  • ? Reducers – Pure functions that take the current state and an action, then return the next state.
  • ? Selectors – Functions that compute derived data from the store.
  • ⚡ Effects – Handle side effects like HTTP calls, logging, or navigation in response to actions.

? Syntax & Theory

When testing NgRx, the core idea is to treat each piece as a pure unit wherever possible:

  • Actions: Assert that action creators return objects with the correct type and payload shape.
  • Reducers: Pass in a known state and action, then assert on the resulting state.
  • Selectors: Call the projector or selector with a mock state and verify the computed value.
  • Effects: Use a mock Actions stream and assert which actions or side effects are triggered.

? Testing Actions

Ensure that your action creators produce the correct type and payload so reducers and effects can rely on them.

? View Code Example
// counter.actions.ts - defines action creators
import { createAction, props } from '@ngrx/store';

export const increment = createAction('[Counter] Increment');
export const add = createAction('[Counter] Add', props<{ value: number }>());

// counter.actions.spec.ts - unit tests for actions
import * as CounterActions from './counter.actions';

describe('Counter Actions', () => {
  it('should create increment action', () => {
    const action = CounterActions.increment();
    expect(action.type).toBe('[Counter] Increment');
  });

  it('should create add action with payload', () => {
    const action = CounterActions.add({ value: 5 });
    expect(action.value).toBe(5);
  });
});

? Testing Reducers

Reducers are pure functions, so you simply pass in a state and action and assert the new state.

? View Code Example
// counter.reducer.ts - reducer implementation
import { createReducer, on } from '@ngrx/store';
import * as CounterActions from './counter.actions';

export const initialState = 0;

export const counterReducer = createReducer(
  initialState,
  on(CounterActions.increment, state => state + 1),
  on(CounterActions.add, (state, { value }) => state + value)
);

// counter.reducer.spec.ts - unit tests for reducer
describe('Counter Reducer', () => {
  it('should increment state', () => {
    const state = counterReducer(0, CounterActions.increment());
    expect(state).toBe(1);
  });

  it('should add value to state', () => {
    const state = counterReducer(1, CounterActions.add({ value: 5 }));
    expect(state).toBe(6);
  });
});

? Testing Selectors

Selectors compute derived state. Test them with a mock state to ensure they return the expected value.

? View Code Example
// counter.selectors.ts - selectors for derived state
import { createSelector } from '@ngrx/store';

export const selectCounter = (state: any) => state.counter;

export const selectDouble = createSelector(
  selectCounter,
  counter => counter * 2
);

// counter.selectors.spec.ts - unit tests for selectors
describe('Counter Selectors', () => {
  it('should select double of state', () => {
    const result = selectDouble.projector(3);
    expect(result).toBe(6);
  });
});

⚡ Testing Effects

Effects react to actions and perform side effects. Use a mock Actions stream and assert the outcome.

? View Code Example
// counter.effects.ts - logs every increment action
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { map } from 'rxjs/operators';
import * as CounterActions from './counter.actions';

@Injectable()
export class CounterEffects {
  logIncrement$ = createEffect(() =>
    this.actions$.pipe(
      ofType(CounterActions.increment),
      map(action => {
        console.log('Increment action dispatched!');
        return action;
      })
    ),
    { dispatch: false }
  );

  constructor(private actions$: Actions) {}
}

? Live Explanation

  • Actions: The tests verify that increment() and add() produce actions with the correct type and payload. If the string changes accidentally, the test will fail.
  • Reducers: By calling counterReducer() directly, you confirm that the state transitions from 0 → 1 and 1 → 6 for the tested actions.
  • Selectors: The projector test for selectDouble checks that the derived state is correct (3 becomes 6), ensuring components that consume it receive the right data.
  • Effects: The effect listens for increment actions and logs a message. In real tests you would mock console.log or other side-effect services and assert that they are called.

✅ Tips & Best Practices

  • Use a testing framework like Jasmine or Jest for all NgRx unit tests.
  • Keep tests isolated – do not depend on Angular components when testing actions, reducers, selectors, or effects.
  • For effects, mock heavy dependencies (like HttpClient or Router) instead of calling them directly.
  • Avoid testing the entire store when you only need to test a single unit (reducer, selector, or effect).
  • Always mock dependencies in effects (services, router, logger) to prevent flaky or slow tests.
  • Do not forget selector tests—broken selector logic can silently break multiple components that depend on derived state.

? Try It Yourself

  • Create a counter feature with increment and add actions and write reducer tests to verify all state transitions.
  • Implement a selector selectDouble (or selectTriple) and test it using the selector projector.
  • Write an effect that logs a message or triggers navigation on a specific action, then test its observable flow using a mocked Actions stream.
  • Extend the feature with a new decrement action, update the reducer and selectors, and add corresponding unit tests.