How to Unit Test an NgRx Reducer
A Really Simple Guide
Introduction
Before we start unit testing reducers, we’ll begin with a quick recap of NgRx reducers.
Reducers in NgRx are responsible for transitioning your application from one state to the next. Reducer functions take care of these transitions by deciding which actions to do based on the type.
Each reducer function looks at:
- the most recent Action sent
- the current state
In this tutorial, we’ll add unit tests for the reducer function that handles these transitions for API success/failure actions.
Example
In this example, we begin by defining a Product class with a single id property to keep the tutorial simple:
export class Product {
id: number | null;
}
In a file called product-api.actions.ts, we define our API success and failure actions for loading a list of products:
import { Product } from '../../product';
import { createAction, props } from '@ngrx/store';export const loadProductsSuccess = createAction(
'[Product API] Load Success',
props<{ products: Product[] }>()
);export const loadProductsFailure = createAction(
'[Product API] Load Fail',
props<{ error: string }>()
);
The product.reducer.ts file below defines the initial state and a product reducer function that handles the transitions from one state to another, depending on which action is handled i.e. whether the products were loaded successfully or not.
The initialState defines the initial value of the product state, with no products and an empty error string by default as expected.
If products are loaded successfully, the ProductState is replaced with the current state (…state) but with products populated and error message emptied.
Otherwise, if there was a problem loading products, the ProductState is replaced with the current state but with no products and an error message.
import { Product } from '../product';
import { createReducer, on } from '@ngrx/store';
import { ProductApiActions } from './actions';export interface ProductState {
id: number | null;
products: Product[];
error: string;
}export const initialState: ProductState = {
id: null,
products: [],
error: ''
};export const productReducer = createReducer<ProductState>(
initialState,
on(ProductApiActions.loadProductsSuccess, (state, action): ProductState => {
return {
...state,
products: action.products,
error: ''
};
}),
on(ProductApiActions.loadProductsFailure, (state, action): ProductState => {
return {
...state,
products: [],
error: action.error
};
})
);
Tests
In a new file product.reducer.spec.ts, we define our two tests for product load success/failure API actions.
For the success test, we add a single product with product id 1 to an array, checking that the result from the product reducer is the same.
For the failure test, we simply check that the error message matches that of the action.
import { ProductApiActions } from "./actions";
import * as fromProducts from './product.reducer';
import { Product } from "../product";describe('Product reducers', () => {
it('[Product API] Load Products Success', () => {
// arrange
const product = new Product();
product.id = 1;
const products = [product]; const action = ProductApiActions.loadProductsSuccess(
{ products }
);
const expectedState: fromProducts.ProductState = {
...fromProducts.initialState,
products
};
// act
const result = fromProducts
.productReducer(fromProducts.initialState, action); // assert
expect(result).toEqual(expectedState);
}); it('[Product API] Load Products Failure', () => { // arrange
const error = 'error';
const action = ProductApiActions.loadProductsFailure(
{
error
}); const expectedState: fromProducts.ProductState = {
...fromProducts.initialState,
error
}; // act
const result = fromProducts
.productReducer(fromProducts.initialState, action); // assert
expect(result).toEqual(expectedState);
});
});
Both tests are passing, as shown below:
Thanks for reading! Let me know what you think in the comments section below, and don’t forget to subscribe. 👍