Because reducers are simple pure functions, it is very easy to test them. The way it works is, when an action is dispatched, the reducer does its magic and returns a new value. This is what we want to test. Let’s get started!

Actions

export const LOAD_USERS = '[Users] Load';
export const LOAD_USERS_FAIL = '[Users] Load Fail';
export const LOAD_USERS_SUCCESS = '[Users] Load Success';

export class LoadUsers implements Action {
  readonly type = LOAD_USERS;
}

export class LoadUsersFail implements Action {
  readonly type = LOAD_USERS_FAIL;
  constructor(public payload: any) {}
}

export class LoadUsersSuccess implements Action {
  readonly type = LOAD_USERS_SUCCESS;
  constructor(public payload: User[]) {}
}

Reducers

export interface UsersState {
  users: { [id: number]: User };
  loading: boolean;
}

export const initialState: UsersState = {
  users: {},
  loading: false,
};

export function reducer(
  state = initialState,
  action: fromUsers.UsersAction
): UsersState {
  switch (action.type) {
    case fromUsers.LOAD_USERS: {
      return {
        ...state,
        loading: true,
      };
    }

    case fromUsers.LOAD_USERS_SUCCESS: {
      const users = action.payload.reduce(
        (users: { [id: number]: Users }, user: User) => {
          return {
            ...users,
            [user.id]: user,
          };
        },
        {
          ...state.users,
        }
      );

      return {
        ...state,
        loading: false,
        users,
      };
    }

    case fromUsers.LOAD_USERS_FAIL: {
      return {
        ...state,
        loading: false,
      };
    }
  }

  return state;
}
  

Testing

Now that we have the actions and reducers, let’s create a basic test file which checks for the initial state

import * as fromUsers from './users.reducer';
import * as fromActions from '../actions/users.action';
import { User } from '../../models/user.model';

describe('UsersReducer', () => {
  it('should return the default state', () => {
    const { initialState } = fromUsers;
    const action = {};
    const state = fromUsers.reducer(undefined, action);

    /**
    Because it doesn't match any of the cases in the reducer file,
    it will return the initialState which is:
    {
      users: {},
      loading: false,
    }
    **/

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

Next step is to test all 3 cases in the reducer file which are: LOAD_USERS, LOAD_USERS_SUCCESS, LOAD_USERS_FAIL.

describe('LOAD_USERS action', () => {
  it('should set loading to true', () => {
    const { initialState } = fromUsers;
    const action = new fromActions.LoadUsers();
    const state = fromUsers.reducer(initialState, action);

    expect(state.loading).toEqual(true);
    expect(state.users).toEqual({});
  });
});
describe('LOAD_USERS_SUCCESS action', () => {
  it('should populate users from the array', () => {
    const users: User[] = [
      { id: 1, name: 'Tom Jones' },
    ];
    const users = {
      1: users[0],
    };
    const { initialState } = fromUsers;
    const action = new fromActions.LoadUsersSuccess(users);
    const state = fromUsers.reducer(initialState, action);

    expect(state.loading).toEqual(false);
    expect(state.users).toEqual(users);
  });
});
describe('LOAD_USERS_FAIL action', () => {
  it('should return the previous state', () => {
    const { initialState } = fromUsers;
    const previousState = { ...initialState, loading: true };
    const action = new fromActions.LoadUsersFail({});
    const state = fromUsers.reducer(previousState, action);

    expect(state).toEqual(initialState);
  });
});

I think the code above is very easy to understand and there is no need for explanation but if you have any questions please leave a comment below.