Testing Reducers page

Learn how to write unit tests for NgRx Reducers.

Quick Start: You can checkout this branch to get your codebase ready to work on this section.

Overview

  1. Verify Login State updates properly when the LoginActions.loginSuccess Action dispatches.

  2. Verify Login State resets properly when the LoginActions.logoutSuccess Action dispatches.

Running Tests

To run unit tests in your project, you can either use the test npm script, or the ng test command:

npm run test
# or
ng test --watch

The --watch switch will rerun your tests whenever a code file changes. You can skip it to just run all tests once.

Description

When testing a Reducer, we will test each of its on() handlers by verifying each pure function returns the expected value given an Action and a Login State (typically the initialState). Unlike the previous testing sections, when testing the Reducer, we don’t need a TestBed since there shouldn’t be any dependencies when dealing with the Reducer.

Update login.selectors.spec.ts

Before we can test our Reducer, our tests involving our Selectors are failing. This is because we’ve changed the shape and initial value of our Login State. To continue, we need to update them. Copy the following code to replace the contents of src/app/store/login/login.selectors.spec.ts:

src/app/store/login/login.selectors.spec.ts

// src/app/store/login/login.selectors.spec.ts

import * as fromLogin from './login.reducer';
import { selectLoginState } from './login.selectors';

describe('Login Selectors', () => {
  it('should select the feature state', () => {
    const result = selectLoginState({
      [fromLogin.loginFeatureKey]: {
        ...fromLogin.initialState,
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      },
    });

    expect(result).toEqual({
      userId: 'some-user-id',
      username: 'some-username',
      token: 'some-token',
    });
  });
});

// src/app/store/login/login.selectors.spec.ts

import * as fromLogin from './login.reducer';
import { selectLoginState } from './login.selectors';

describe('Login Selectors', () => {
  it('should select the feature state', () => {
    const result = selectLoginState({
      [fromLogin.loginFeatureKey]: {
        ...fromLogin.initialState,
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      },
    });

    expect(result).toEqual({
      userId: 'some-user-id',
      username: 'some-username',
      token: 'some-token',
    });
  });
});

We will go over these changes in the upcoming section where we go over testing Selectors (Testing Selectors). For now after you update src/app/store/login/login.selectors.spec.ts, all of your tests should pass.

Update login.reducer.spec.ts

We will walk through updating src/app/store/login/login.reducer.spec.ts to run tests for your Reducer.

Verify Login State Updates Properly When the LoginActions.loginSuccess Action Dispatches

To test the on() handler associated with LoginActions.loginSuccess, we will define 3 values:

  1. Expected Login State after Reducer handles Action.

  2. The Action we are testing.

  3. The generated Login State after passing initialState and Action to reducer().

src/app/store/login/login.reducer.spec.ts

// src/app/store/login/login.reducer.spec.ts

import { reducer, initialState, State } from './login.reducer';
import * as LoginActions from './login.actions';

describe('Login Reducer', () => {
  describe('loginSuccess action', () => {
    it('should update the state in an immutable way', () => {
      // Expectation of new state
      const expectedState: State = {
        ...initialState,
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      };

      const action = LoginActions.loginSuccess({
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      });

      const state = reducer({ ...initialState }, action);
    });
  });

  describe('an unknown action', () => {
    it('should return the previous state', () => {
      const action = {} as any;

      const result = reducer(initialState, action);

      expect(result).toBe(initialState);
    });
  });
});

Next we will add our expectations to verify that our expected Login State matches the generated Login State. When we do this, we will also verify that we do this in an immutable way to maintain an immutable data structure:

src/app/store/login/login.reducer.spec.ts

// src/app/store/login/login.reducer.spec.ts

import { reducer, initialState, State } from './login.reducer';
import * as LoginActions from './login.actions';

describe('Login Reducer', () => {
  describe('loginSuccess action', () => {
    it('should update the state in an immutable way', () => {
      // Expectation of new state
      const expectedState: State = {
        ...initialState,
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      };

      const action = LoginActions.loginSuccess({
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      });

      const state = reducer({ ...initialState }, action);

      // Compare new state
      expect(state).toEqual(expectedState);

      // Check for immutability
      expect(state).not.toBe(expectedState);
    });
  });

  describe('an unknown action', () => {
    it('should return the previous state', () => {
      const action = {} as any;

      const result = reducer(initialState, action);

      expect(result).toBe(initialState);
    });
  });
});

Verify Login State Resets Properly When the LoginActions.logoutSuccess Action Dispatches

We will do the same when testing the on() handler associated with LoginActions.logoutSuccess, but instead of defining an expected Login State, we will just use the initialState since it already is our expected Login State. And instead of passing our initialState to the reducer(), we will pass some updated Login State instead to ensure that all values are reset properly:

src/app/store/login/login.reducer.spec.ts

// src/app/store/login/login.reducer.spec.ts

import { reducer, initialState, State } from './login.reducer';
import * as LoginActions from './login.actions';

describe('Login Reducer', () => {
  describe('loginSuccess action', () => {
    it('should update the state in an immutable way', () => {
      // Expectation of new state
      const expectedState: State = {
        ...initialState,
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      };

      const action = LoginActions.loginSuccess({
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      });

      const state = reducer({ ...initialState }, action);

      // Compare new state
      expect(state).toEqual(expectedState);

      // Check for immutability
      expect(state).not.toBe(expectedState);
    });
  });

  describe('logoutSuccess action', () => {
    it('should reset LoginState to initialState', () => {
      const action = LoginActions.logoutSuccess();

      const state = reducer(
        {
          ...initialState,
          userId: 'some-user-id',
          username: 'some-username',
          token: 'some-token',
        },
        action
      );

      // Compare new state
      expect(state).toEqual(initialState);

      // Check for immutability
      expect(state).not.toBe(initialState);
    });
  });

  describe('an unknown action', () => {
    it('should return the previous state', () => {
      const action = {} as any;

      const result = reducer(initialState, action);

      // Shouldn’t update state at all
      expect(result).toBe(initialState);
    });
  });
});

Final Result

At the end of this section, the following spec file(s) should be updated. After each spec file has been updated and all the tests have passed, this means that all the previous sections have been completed successfully:

src/app/store/login/login.reducer.spec.ts

// src/app/store/login/login.reducer.spec.ts

import { reducer, initialState, State } from './login.reducer';
import * as LoginActions from './login.actions';

describe('Login Reducer', () => {
  describe('loginSuccess action', () => {
    it('should update the state in an immutable way', () => {
      // Expectation of new state
      const expectedState: State = {
        ...initialState,
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      };

      const action = LoginActions.loginSuccess({
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      });

      const state = reducer({ ...initialState }, action);

      // Compare new state
      expect(state).toEqual(expectedState);

      // Check for immutability
      expect(state).not.toBe(expectedState);
    });
  });

  describe('logoutSuccess action', () => {
    it('should reset LoginState to initialState', () => {
      const action = LoginActions.logoutSuccess();

      const state = reducer(
        {
          ...initialState,
          userId: 'some-user-id',
          username: 'some-username',
          token: 'some-token',
        },
        action
      );

      // Compare new state
      expect(state).toEqual(initialState);

      // Check for immutability
      expect(state).not.toBe(initialState);
    });
  });

  describe('an unknown action', () => {
    it('should return the previous state', () => {
      const action = {} as any;

      const result = reducer(initialState, action);

      // Shouldn’t update state at all
      expect(result).toBe(initialState);
    });
  });
});

src/app/store/login/login.selectors.spec.ts

// src/app/store/login/login.selectors.spec.ts

import * as fromLogin from './login.reducer';
import { selectLoginState } from './login.selectors';

describe('Login Selectors', () => {
  it('should select the feature state', () => {
    const result = selectLoginState({
      [fromLogin.loginFeatureKey]: {
        ...fromLogin.initialState,
        userId: 'some-user-id',
        username: 'some-username',
        token: 'some-token',
      },
    });

    expect(result).toEqual({
      userId: 'some-user-id',
      username: 'some-username',
      token: 'some-token',
    });
  });
});

Wrap-up: By the end of this section, your code should match this branch. You can also compare the code changes for our solution to this section on GitHub or you can use the following command in your terminal:

git diff origin/test-reducer