How to Unit Test Angular NgRx Effects

A Step-by-Step Guide

George Marklow

--

Photo by Steve Johnson on Unsplash

Introduction

Effects are where you manage things like requesting data, long-running processes that create several events, and other external interactions that your components don’t need to be aware of. Read more here.

This tutorial shows you how to write unit tests for NgRx effects.

Installation

You can skip these steps if you already have a tutorial Angular project and NgRx set up.

First, create a new angular project.

Open the terminal (⌘ + Space, then type terminal).

Create a new directory somewhere on your drive called tutorial and enter that directory:

mkdir tutorial
cd tutorial

Then, create a new Angular project called shop (just select Y and use CSS when presented with the options):

ng new shop

For the next steps, you’ll need to enter the shop application directory:

cd shop

Install NgRx Schematics (provides Angular CLI commands for generating files when building new NgRx feature areas):

ng add @ngrx/schematics

Select Y asked if you use @ngrx/schematics as the default collection:

Next, install the NgRx Store (a state container):

ng add @ngrx/store

Then install NgRx Effects:

ng add @ngrx/effects

The last npm package we need is TypeMoq, which we’ll need later when setting up the effects tests:

npm install typemoq

Use the “code .” command to open VS Code:

code .

Note: if VS Code doesn’t open, check the following article: https://code.visualstudio.com/docs/setup/mac

As a sanity check, run the application to check everything works fine. I prefer to open a terminal window inside VS code from this point on:

ng serve --open

Note: the ng serve command builds the app and development server and will rebuild if there are any code changes.

The --open flag opens a browser to http://localhost:4200.

If all is well, you should see a browser window open:

Add Feature Module with Store

To keep this a realistic example, let’s create a feature module and add a Store folder where we can add effects. This is like a substate of the main application state that has effects specific to that module.

Open a new terminal window in VS and enter the app folder:

cd src
cd app

To make things easier, the following commands can all be run from this current location, meaning you don’t need to worry about switching directories or manually adding folders and files 👏.

Create a new feature module called ProductModule. This will create a subfolder called “products”:

ng g m Products

Next, add an NgRx store called ProductState:

ng g store ProductState --statePath=products/store --module=products/products.module.ts --stateInterface=ProductState

Add Effects

Run the command to add an effects folder inside the store folder, which automatically creates effects files and tests:

ng g effect products/store/effects/product --module=products/products.module.ts

Just select Y to the options and leave the prefix blank.

Add the following loadProducts$ effect, returning null for the moment while we add API and page actions and unit tests:

import { Injectable } from '@angular/core';
import { Actions, createEffect } from '@ngrx/effects';
@Injectable()
export class ProductEffects {
constructor(private actions$: Actions) {}
loadProducts$ = createEffect(() =>
{
return null;
}
);
}

Add Actions

Run this command to generate page actions:

ng g action products/store/actions/product-page? What should be the prefix of the action? load
? Should we generate success and failure actions? Yes
? Do you want to use the create function? Yes

Update the page actions file to define just a single page action for loading products:

import { createAction } from '@ngrx/store';export const loadProducts = createAction(
'[ProductApi] Load Products'
);

Next, add API actions:

ng g action products/store/actions/product-api? What should be the prefix of the action? load
? Should we generate success and failure actions? Yes
? Do you want to use the create function? Yes

Update the file to define the success and failure API actions.

On success, we return an array of products; on failure, we simply return an error string:

import { createAction, props } from '@ngrx/store';import { Product } from '../../models/product';export const loadProductsSuccess = createAction('[ProductApi] Load Products Success',
props<{ products: Product[] }>()
);
export const loadProductsFailure = createAction('[ProductApi] Load Products Failure',
props<{ error: string }>()
);

Finally, run this command to generate the index.ts file:

touch products/store/actions/index.ts

Open the index.ts file and export the page and API actions:

import * as ProductPageActions from './product-page.actions'
import * as ProductApiActions from './product-api.actions'
export {
ProductApiActions,
ProductPageActions
}

Add a Model

Add a Product model to a new models folder:

mkdir products/models
touch products/models/product.ts

Create a Product class. We won’t need to add any properties to the class for the scope of this article.

export class Product { }

Add a Service

We need to add a service that can be mocked in the effects test cases, so let’s just add a dummy service and a single endpoint that just returns an empty observable (this tutorial focuses only NgRx effects).

First, create a ProductService:

ng g service products/services/product

Then, add a getProducts function that should return an array of products. For the purposes of this tutorial, return an empty observable:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Product } from '../models/product'
import { EMPTY } from 'rxjs'
import { HttpClient } from '@angular/common/http'
@Injectable({
providedIn: 'root'
})
export class ProductService { constructor(private http: HttpClient) { } getProducts(): Observable<Product[]> {
return EMPTY;
}
}

Here, I’ve also injected the httpClient into the constructor. We won’t use this here but of course, in the real world, this service would use to make API calls.

Add Test Cases to Effects

Now open the product.effects.spec.ts test file and add two test cases for success and failure API actions.

Both tests will fail of course as we haven’t yet provided the implementation.

import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable, of, throwError } from 'rxjs';
import { ProductService } from '../../services/product.service';
import * as TypeMoq from 'typemoq';
import { ProductEffects } from './product.effects';
import { Product } from '../../models/product';
import { ProductApiActions, ProductPageActions } from '../actions/index';
describe('ProductEffects', () => {
let actions$: Observable<any>;
let effects: ProductEffects;
let mockProductService: TypeMoq.IMock<ProductService>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
ProductEffects,
{ provide: ProductService, useFactory: () => mockProductService.object },
provideMockActions(() => actions$)
]
});
effects = TestBed.inject(ProductEffects);
});
it('loadProducts$ should return [Product API] Load Products Success on success', () => {

// arrange
const products = [new Product()];
mockProductService
.setup(x => x.getProducts())
.returns(() => of(products))
.verifiable();
const expectedAction = ProductApiActions.loadProductsSuccess({ products }); // act
actions$ = of(ProductPageActions.loadProducts());
// assert
effects.loadProducts$.subscribe(x => {
expect(x).toEqual(expectedAction);
});

mockProductService.verifyAll();
});
it('loadProducts$ should return [Product API] Load Products Failure on failure', () => { // arrange
const error = 'error';
mockProductService
.setup(x => x.getProducts())
.returns(() => throwError(error))
.verifiable();
const expectedAction = ProductApiActions.loadProductsFailure({ error }); // act
actions$ = of(ProductPageActions.loadProducts());
// assert
effects.loadProducts$.subscribe(x => {
expect(x).toEqual(expectedAction);
});
mockProductService.verifyAll();
});
});

Provide Implementation for Effects

Now provide an implementation for the loadProducts$ effect, handling the success and fail API actions with appropriate payloads.

Re-run the tests to check that they pass.

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { switchMap, catchError, map } from 'rxjs/operators';
import { ProductService } from '../../services/product.service';
import { ProductApiActions, ProductPageActions } from '../actions';
@Injectable()
export class ProductEffects {
constructor(
private productService: ProductService,
private actions$: Actions) {}
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductPageActions.loadProducts),
switchMap(_ =>
this.productService.getProducts()
.pipe(map(products => ProductApiActions.loadProductsSuccess({ products })), catchError(error => of(ProductApiActions.loadProductsFailure({ error })))))));
}

Thanks for reading! Let me know what you think in the comments section below, and don’t forget to subscribe. 👍

--

--

George Marklow

George is a software engineer, author, blogger, and abstract artist who believes in helping others to make us happier and healthier.