Brian Love
Google Developer Expert in Angular, software engineer and skier located in Denver, CO

NgRx Testing: Reducers

Reading time ~11 minutes

Learn how to unit test NgRx reducers using Jest.

The goal is to assert the mutation to the state of the application as a result of dispatching an action. 🏋️🏋️💪💪

Download

In this post I’ll be working with a demo application. You can clone the repository or download a zip file of the source code:

Series

This post is part of a series on testing NgRx using Jest:

Stack

The stack for this application will be:

  • Angular 6
  • NgRx 6
  • Jest and the angular-jest-preset module
  • The jasmine-marbles module
  • Faker for creating fake data

Jest Test Runner

As I mentioned above, this post will be using the Jest test runner. It’s much faster than Karma (even with headless Chrome) and uses a Jasmine-like API. In fact, if most of your tests were generated by the CLI then you may be able to simply swap out Karma for Jest.

If you’re new to Jest, check out my post on using Jest with Angular.

NOOP Action

First, we should assert that the state is not mutated for an invalid or NOOP action in the reducer() function in src/app/state/user/user.reducer.spec.ts:

describe('undefined action', () => {
  it('should return the default state', () => {
    const action = { type: 'NOOP' } as any;
    const result = reducer(undefined, action);

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

Let’s review the test above:

  • Within the “should return the default state” spec we create a new action, whose type is set to “NOOP”.
  • We then invoke the reducer() function specifying undefined for the current state along with the noop action.
  • Finally, we expect that the resulting state is the same as the initialState, which is the default value for the state argument in the reducer() function.

If we run our tests, it should pass:

$ npm test

AddUser Action

In the example app I have specified some actions for adding a user in src/app/state/user/user.actions.ts:

export enum UserActionTypes {
  AddUser = '[User] Add User',
  AddUserSuccess = '[User] Add User Success',
  AddUserFail = '[User] Add User Fail',
}

In the src/app/users/containers/add/add.component.ts component we dispatch() the AddUser action. This will toggle the loading boolean property in state, and the addUser effect in src/app/store/user/user.effects.ts will invoke the UserService.addUser() method, which will send a PUT request to our API to create a new user entity. When the request completes, we’ll dispatch the AddUserSuccess action, or if the response was an error (e.g. 500 status code) we’ll dispatch the AddUserFail action.

Here is the definition of the AddUser action:

export class AddUser implements Action {
  readonly type = UserActionTypes.AddUser;

  constructor(public payload: { user: Partial<User> }) {}
}

For the payload we simply define an object that must have the user property, which is a Partial of a User.

Next, let’s take a look at the reducer() implementation for this action:

export function reducer(state = initialState, action: UserActions): State {
  switch (action.type) {
    case UserActionTypes.AddUser:
      return { ...state, loading: true, error: undefined };
  }
}

When the AddUser action is dispatched we are updating the state so that the loading boolean value is set to true and ensuring that the error property is set to undefined.

Here is the test for the AddUser action in the src/app/state/user/user.reducer.spec.ts file:

describe('User Reducer', () => {
  describe('[User] Add User', () => {
    it('should toggle loading state', () => {
      const action = new AddUser({ user });
      const result = reducer(initialState, action);

      expect(result).toEqual({
        ...initialState,
        error: undefined,
        loading: true
      });
    });
  });
});

Let’s review the test:

  • First, all of our tests are within the “User Reducer” test suite.
  • Within the test, we new up the AddUser action, specifying a generated user that we created using faker.
  • We then invoke the reducer() function, providing the initialState and the action.
  • Finally, we expect the result to equal a new object; first spreading the initialState object, and then specifying the error and loading property values.

AddUserSuccess Action

Next, let’s assert that the AddUserSuccess action returns the new state with the newly created user entity added.

In this demo application I am using the @ngrx/entity module for managing a collection of User objects. While it is probably not necessary to assert state mutations that solely use the entity adapter, I wanted to explicitly test each action, including those that solely/primarily use the adapter’s methods (e.g. addOne()) for mutating the state of the application.

In general, I think it’s safe to say:

Assume @ngrx/entity adapter state mutations are battle-tested and likely do not need to be tested in your application

First, here is the definition of the AddUserSuccess action in src/app/state/user/user.actions.ts:

export class AddUserSuccess implements Action {
  readonly type = UserActionTypes.AddUserSuccess;

  constructor(public payload: { user: User }) {}
}

And, here is the case-statement in the reducer() for the AddUserSuccess action that returns the new state:

case UserActionTypes.AddUserSuccess:
case UserActionTypes.LoadUserSuccess: {
  return adapter.addOne(action.payload.user, { ...state, loading: false });
}

As you can see, we invoke the addOne() method on the entity adapter, providing the user that was created (or loaded) and toggle the loading boolean property to false.

Now, let’s look at the test in the src/app/state/user/user.reducer.spec.ts file:

describe('[User] Add User Success', () => {
  it('should add a user to state', () => {
    const action = new AddUserSuccess({ user });
    const result = reducer(initialState, action);

    expect(result).toEqual({
      ...initialState,
      entities: {
        [user.id]: user
      },
      ids: [user.id],
      loading: false
    });
  });
});

In this test we invoke the reducer() function, specifying the initialState along with the AddUserSuccess action that we created with the necessary payload. We then expect() the result to equal the initialState with the additional entities and ids properties that are created via the adapter’s addOne() method.

We also assert that the loading boolean property has been set to false. This is really the only “manual” state mutation that is performed in the reducer that deserves to be tested.

AddUserFail Action

We should also test the failure path for when the HTTP request for adding a new user fails. To do this, we’ll be asserting that the AddUserFail action results in a new state of our application where the error property contains the Error that occurred.

First, here is the definition of the AddUserFail action in src/app/state/user/user.actions.ts:

export class AddUserFail implements Action {
  readonly type = UserActionTypes.AddUserFail;

  constructor(public payload: { error: Error }) {}
}

And, here is the case-statement in the reducer() function:

case UserActionTypes.AddUserFail:
case UserActionTypes.AddUsersFail:
case UserActionTypes.LoadUserFail:
case UserActionTypes.LoadUsersFail:
case UserActionTypes.UpdateUserFail:
case UserActionTypes.UpdateUsersFail: {
  return { ...state, error: action.payload.error, loading: false };
}

You’ll note that in this application we have the same result when any of the actions fail: we update the error property to contain the Error that was dispatched in the action’s payload and toggle the loading boolean to false.

Now, let’s assert that the state is mutated appropriately when the AddUserFail action is dispatched:

describe('[User] Add User Fail', () => {
  it('should update error in state', () => {
    const error = new Error();
    const action = new AddUserFail({ error });
    const result = reducer(initialState, action);

    expect(result).toEqual({
      ...initialState,
      error,
      loading: false
    });
  });
});

This should start to look familar. We wire up the AddUserFail action, invoke the reducer() function, and assert that the result is equal to the expected state of the application.

LoadUser, LoadUserSuccess and LoadUserFail

The tests for the LoadUser, LoadUserSuccess and LoadUserFail are nearly identitical to those for adding a user. Let’s quickly take a look at each one.

Assert that the LoadUser action result equals the expected value where the error property is undefined and the loading property is toggled to true:

describe('[User] Load User', () => {
  it('should toggle loading state', () => {
    const action = new LoadUser({ id: user.id });
    const result = reducer(initialState, action);

    expect(result).toEqual({
      ...initialState,
      error: undefined,
      loading: true
    });
  });
});

Assert that the LoadUserSuccess action result equals the expected value where the entities and ids properties contain the user that was dispatched in the action’s payload:

describe('[User] Load User Success', () => {
  it('should load a user to state', () => {
    const action = new LoadUserSuccess({ user });
    const result = reducer(initialState, action);

    expect(result).toEqual({
      ...initialState,
      entities: {
        [user.id]: user
      },
      ids: [user.id],
      loading: false
    });
  });
});

Assert that the LoadUserFail action result equals the expected value where the error property contains the Error that was dispatched in the action’s payload:

describe('[User] Load User Fail', () => {
  it('should update error in state', () => {
    const error = new Error();
    const action = new LoadUserFail({ error });
    const result = reducer(initialState, action);

    expect(result).toEqual({
      ...initialState,
      error,
      loading: false
    });
  });
});

LoadUsers, LoadUsersSuccess and LoadUsersFail

The unit tests for the LoadUsers, LoadUsersSuccess and LoadUsersFail actions are similar, with the exception that the entities and ids properties are updated with many User objects.

We’ll skip the LoadUsers and LoadUsersFail tests for the sake of brevity as they are nearly identical to the LoadUser and LoadUserFail tests.

Let’s quickly look at testing the LoadUsersSuccess action:

describe('[User] Load Users Success', () => {
  it('should add all users to state', () => {
    const users = [user];
    const action = new LoadUsersSuccess({ users });
    const result = reducer(initialState, action);

    expect(result).toEqual({
      ...initialState,
      entities: users.reduce(
        (entityMap, user) => ({
          ...entityMap,
          [user.id]: user
        }),
        {}
      ),
      ids: users.map(user => user.id),
      loading: false
    });
  });
});

In the expect() assertion we use the Array.prototype.reduce() method to create the entities dictionary of user objects.

The @ngrx/entity library uses the following signature to store the entities in state for fast retrieval of objects:

export declare type DictionaryNum<T> = {
    [id: number]: T;
};
export declare abstract class Dictionary<T> implements DictionaryNum<T> {
    [id: string]: T;
}

Whether the unique identifier is a number or a string, the unique identifier is the property name (or key) in the entities object.

This is why we use the reduce() method to create the entities object that matches the DictionaryNum or Dictionary type.

The ids array is simply an array of the id values. We use the map() method to map the values within the users array of User objects.

UpdateUserSuccess

The tests for the UpdateUser and UpdateUserFail actions are also mostly identical to the existing tests that we have looked at where we dispatch an action that toggles the loading property and update the error property.

On the other hand, wiring up a test that asserts the UpdateUserSuccess action mutates the state of our application is a bit more complex, and quite frankly, a bit verbose:

describe('User Reducer', () => {
  const user: User = {
    id: 1,
    firstName: 'Anakin',
    lastName: 'Skywalker'
  };

  describe('[User] Update User Success', () => {
    it('should update user in state', () => {
      const updatedUser: User = {
        ...user,
        firstName: 'Darth',
        lastName: 'Vader'
      };
      const action = new UpdateUserSuccess({
        update: {
          id: user.id,
          changes: updatedUser
        }
      });

      const state = reducer(initialState, new AddUserSuccess({ user }));
      expect(state).toEqual({
        ...initialState,
        entities: {
          [user.id]: user
        },
        ids: [user.id],
        loading: false
      });

      const result = reducer(state, action);
      expect(result).toEqual({
        ...state,
        entities: {
          ...state.entities,
          [user.id]: updatedUser
        },
        ids: [...state.ids],
        loading: false
      });
    });
  });
});

Here is what the UpdateUserSuccess test is doing:

  • First, we define an updatedUser object that uses the current user object properties modifying firstName and lastName property values. We’re going for a bit of a Star Wars theme here. If you’re opposed to Star Wars, just go along with it for this test. And if you’re a fan, hopefully you’ll enjoy the bit of fun we’ll be having writing unit tests for our Angular application.
  • We then new up an UpdateUserSuccess action with the update property. Again, we’re using the @ngrx/entity library, so we specify an Update type signature with the id and changes properties.
  • We invoke the reducer() function providing the initialState and a new AddUserSuccess action.
  • We assert that the AddUserSuccess action appropriately returns a new state object with a new user added.
  • Then, we invoke the reducer() function again. This time we provide the state that was a result of the AddUserSuccess action, along with the action instance of UpdateUserSuccess.
  • Finally, we assert that the result of the UpdateUserSuccess action represents the updatedUser.

UpdateUsersSuccess

The test for the UpdateUsersSuccess action is similar to the test we just looked at for UpdateUserSuccess with the addition of updating multiple users:

describe('User Reducer', () => {
  const user: User = {
    id: 1,
    firstName: 'Anakin',
    lastName: 'Skywalker'
  };

  describe('[User] Update Users Success', () => {
    it('should add all users to state', () => {
      const senator = {
        id: 2,
        firstName: 'Sheev',
        lastName: 'Palpaatine'
      };
      const vader = {
        ...user,
        firstName: 'Darth',
        lastName: 'Vader'
      };
      const sidious = {
        ...senator,
        firstName: 'Darth',
        lastName: 'Sidious'
      };
      const originalUsers = [user, senator];
      const updatedUsers = [vader, sidious];

      const state = reducer(
        initialState,
        new AddUsersSuccess({
          users: originalUsers
        })
      );

      const action = new UpdateUsersSuccess({
        update: [
          {
            id: user.id,
            changes: vader
          },
          {
            id: senator.id,
            changes: sidious
          }
        ]
      });
      const result = reducer(state, action);

      expect(result).toEqual({
        ...state,
        entities: updatedUsers.reduce(
          (entityMap, user) => ({
            ...entityMap,
            [user.id]: user
          }),
          {}
        ),
        ids: updatedUsers.map(user => user.id),
        loading: false
      });
    });
  });
});

I’m not going to review each line of code, but in general, we want to add the originalUsers to the state of our application and then assert that updating multiple users via the UpdateUsersSuccess action results in the expected state.

SelectUser

The last test in src/app/state/user/user.reducer.spec.ts is to assert that the SelectUser action updates the selectedUser property in our state object:

describe('[User] Select User', () => {
  it('should set the selectedUserId property in state', () => {
    const action = new SelectUser({ id: user.id });
    const result = reducer(initialState, action);

    expect(result).toEqual({
      ...initialState,
      selectedUserId: user.id
    });
  });
});

Run Tests

Finally, we run the tests using:

$ npm test

And we should see that all of our tests pass.

Conclusion

Writing unit tests can be fun! 🤓☝️

No really, unit testing actions in your Angular application that uses NgRx can be accomplished pretty easily. The only tests in these examples that were a bit tedious to write were the update tests, especially updating multiple entities at a time. However, we now have excellent code coverage 🌈☀️😁 and be assured that our actions are appropriately mutating the state via the reducer() function.

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).