Write Angular like a pro. Angular Icon

Follow the ultimate Angular roadmap.

Testing Actions in NGRX Store

In this small NGRX Store testing series, we’re going to learn how to test Actions, Reducers and Selectors. Let’s begin with Actions, or more specifically Action Creators and how to test them.

Testing Actions

Ideally, we just want to validate that our actions are setup correctly - we’re not testing deep “functionality” like a reducer, actions take on a more simple form.

What we’ll test

Before we dive in, let’s look at what we’ll be testing:

// pizzas.action.ts
export const LOAD_PIZZAS = '[Products] Load Pizzas';
export const LOAD_PIZZAS_FAIL = '[Products] Load Pizzas Fail';
export const LOAD_PIZZAS_SUCCESS = '[Products] Load Pizzas Success';

export class LoadPizzas implements Action {
  readonly type = LOAD_PIZZAS;
}

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

export class LoadPizzasSuccess implements Action {
  readonly type = LOAD_PIZZAS_SUCCESS;
  constructor(public payload: Pizza[]) {}
}

We’ve got a blend of typical action constants and action creators. We just want to interact with the action creators when we actually build our application, and we also want to do the same with test!

Separating the action constants out also gives us some additional benefit when it comes to testing actions - and using classes for creators gives us a nice boost with TypeScript for safety.

Spec File

I’m going to assume you know how to setup some tests with Angular, we’re not focusing on that here. I typically create my test files alongside the code I’m testing:

products/store/actions/pizzas.action.ts
products/store/actions/pizzas.action.spec.ts

So let’s start with our friend describe and setup the test cases for each action:

describe('LoadPizzas', () => {
  it('should create an action', () => {});
});

describe('LoadPizzasFail', () => {
  it('should create an action', () => {});
});

describe('LoadPizzasSuccess', () => {
  it('should create an action', () => {});
});

For now, let’s just test our most basic action LoadPizzas. Looking at the action creator, it only accepts a type and no payload. So that’s nice and simple.

Angular Directives In-Depth eBook Cover

Free eBook

Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon Setters and Getters for Styles and Class Bindings

Creating an instance

To test our action, much like when we dispatch inside a component, we need to create a new instance of the class and then simple test our custom object against a typical object that the Redux pattern accepts:

import { LoadPizzas } from './pizzas.action';

describe('LoadPizzas', () => {
  it('should create an action', () => {
    const action = new LoadPizzas();
  });
});

Nice and simple, right?

Assertions

We can next import the LOAD_PIZZAS action constant (because we want to check that our creator is composing the right object for us). Finally, we can finish things off now (but be warned, this below code will error and I’ll explain why after):

import { LoadPizzas, LOAD_PIZZAS } from './pizzas.action';

describe('LoadPizzas', () => {
  it('should create an action', () => {
    const action = new LoadPizzas();

    expect(action).toEqual({ type: LOAD_PIZZAS });
  });
});

This test is pretty much finished, and while it’s technically correct (as it produces an object with a type property) it will fail! Why? Because we are mixing object types and that’s now what our test cases are comparing.

Our const action contains an object identity that looks like this:

LoadPizzas { type: 'LOAD_PIZZAS' }

This is an extremely important piece to note! We are creating a new instance which means we’re creating a new custom object called LoadPizzas. A “normal” object’s identity would look like this:

Object { type: 'LOAD_PIZZAS' }

See the difference? When we create new Object() or even just using the literal {} approach, we create an Object type. The JavaScript engine doesn’t care when executing our code, but we should care for our tests because two objects are never the same.

Running the tests, we get this error:

Expected object to be a kind of Object, but was LoadPizzas { type: '[Products]
Load Pizzas' }

So, what can we do to fix this? There are other ways you could test actions, I just find this the easiest way using toEqual():

describe('LoadPizzas', () => {
  it('should create an action', () => {
    const action = new LoadPizzas();

    expect({ ...action }).toEqual({ type: LOAD_PIZZAS });
  });
});

A simple change. This uses an object literal and spreads the LoadPizzas object into it. Thus rendering the types the exact same, and we have a happy test assertion. There are other ways to do this, without the spread operator, but I find testing it this way is far better than attempting to do something like this:

describe('LoadPizzas', () => {
  it('should create an action', () => {
    const action = new LoadPizzas();

    expect(action.type).toEqual(LOAD_PIZZAS);
  });
});

In the above example it looks like it’d make more sense - but actions are trivial and I find the object literal clearer and easier to manage (as well as read).

So, onto testing the payload. Interestingly, type is the only required property with NGRX Store actions so we don’t technically need to call it “payload” - but I prefer a consistent convention than randomly naming my properties. Plus it’s just more to think about for no apparent gain.

Before we test our LoadPizzasSuccess action, let’s remind ourselves of the action creator:

export class LoadPizzasSuccess implements Action {
  readonly type = LOAD_PIZZAS_SUCCESS;
  constructor(public payload: Pizza[]) {}
}

Okay, so an array of pizzas! This is great as when we also test the action, we’ll see TypeScript benefits in our text editors/IDEs while we write our tests. If you’re supplying it an incorrect data structure, you’re going to see errors.

The setup is pretty simple to test for a payload as well, we just need to somewhat mock the action’s expected data structure:

describe('LoadPizzasSuccess', () => {
  it('should create an action', () => {
    const payload: Pizza[] = [
      {
        id: 1,
        name: 'Pizza #1',
        toppings: [{ id: 1, name: 'onion' }],
      },
      {
        id: 2,
        name: 'Pizza #2',
        toppings: [{ id: 1, name: 'onion' }],
      },
    ];
    const action = new LoadPizzasSuccess(payload);

    expect({ ...action }).toEqual({
      type: LOAD_PIZZAS_SUCCESS,
      payload,
    });
  });
});

Notice how you just pass the payload straight into the LoadPizzasSuccess action, and then use the same payload property to create the object property again inside toEqual().

This makes sure that our action is simply passing things through correctly, and our basic tests simply verify the declarative structure we have is correct. Nothing really more to it.

The key thing to remember is we’re simply testing inputs and outputs here. Does my action creator produce the right object? Yes, or no. However, we also get some TypeScript benefit when writing the tests as well if you write them first, as our actions will then be strict and implement the correct data structures against interfaces.

And that’s pretty much all there is to testing actions! Happy dispatching.

Learn Angular the right way.

The most complete guide to learning Angular ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover