Actions
We begin with the simplest category: the NgRx action definitions themselves. Personally, I see little benefit in writing dedicated unit tests for these. Instead, actions are covered implicitly through the tests written for reducers, effects, and the components that dispatch them.
Reducers
At their core, reducers are synchronous, pure functions. They take the existing state along with an action as arguments and produce a fresh state object in return.
Their pure nature, free from any external dependencies, makes them incredibly straightforward to test. There is no need for TestBed configuration or mocking frameworks. To test a reducer, you simply call it with a known state and action.
The test then asserts that the resulting state matches your expectations, given those specific inputs.
import { createFeature, createReducer } from '@ngrx/store';
import { immerOn } from 'ngrx-immer';
import { customersApiActions, invoicesApiActions, customerPageActions } from './actions';
export const customersInitialState: {
customers: Record<string, Customer>;
invoices: Record<string, Invoice[]>;
} = {
customers: {},
invoices: {},
};
// the customersFeature reducer manages the customers and invoices state
// when a customer or the invoices are fetched, these are added to the state
// when the invoices are collected, the state is of the invoice is updated to 'collected'
export const customersFeature = createFeature({
name: 'customers',
reducer: createReducer(
customersInitialState,
immerOn(customersApiActions.success, (state, action) => {
state.customers[action.customer.id] = action.customer;
}),
immerOn(invoicesApiActions.success, (state, action) => {
state.invoices[action.customerId] = action.invoices;
}),
immerOn(customerPageActions.collected, (state, action) => {
const invoice = state.invoices[action.customerId].find(
(invoice) => invoice.id === action.invoiceId,
);
if (invoice) {
invoice.state = 'collected';
}
}),
),
});
I want to highlight a couple of practices that I find particularly useful:
🔦 Using a factory method to instantiate new state entities. This provides a single point of creation, which simplifies future refactoring when the object's structure evolves. It also allows you to easily create objects with a valid default state while still permitting overrides for specific test scenarios.
🔦 Assigning test data to variables in the arrange phase. This data is then used both to call the reducer (act) and to verify the result (assert). This practice avoids magic values, which can lead to confusion and hard-to-trace test failures when data changes.
import { customersFeature, customersInitialState } from '../reducer';
import { customersApiActions, invoicesApiActions, customerPageActions } from '../actions';
const { reducer } = customersFeature;
it('customersApiActions.success adds the customer', () => {
const customer = newCustomer();
const state = reducer(customersInitialState, customersApiActions.success({ customer }));
expect(state).toEqual({
customers: {
// 🔦 Use the customer variable
[customer.id]: customer,
},
invoices: {},
});
});
it('invoicesApiActions.success adds the invoices', () => {
const invoices = [newInvoice(), newInvoice(), newInvoice()];
const customerId = '3';
const state = reducer(
customersInitialState,
invoicesApiActions.success({ customerId, invoices }),
);
expect(state).toEqual({
customers: {},
invoices: {
// 🔦 Use the customerId and invoices variable
[customerId]: invoices,
},
});
});
it('customerPageActions.collected updates the status of the invoice to collected', () => {
const invoice = newInvoice();
invoice.state = 'open';
const customerId = '3';
const state = reducer(
{ ...customersInitialState, invoices: { [customerId]: [invoice] } },
customerPageActions.collected({ customerId, invoiceId: invoice.id }),
);
expect(state.invoices[customerdId][0]).toBe('collected');
});
// 🔦 A factory method to create a new customer entity (in a valid state)
function newCustomer(): Customer {
return { id: '1', name: 'Jane' };
}
// 🔦 A factory method to create a new invoice entity (in a valid state)
function newInvoice(): Invoice {
return { id: '1', total: 100.3 };
}
Selectors
Selectors are pure functions used to read a specific piece of data from the global NgRx store.
I generally divide selectors into two categories. The first category directly accesses raw data from the state tree. The second category combines data from multiple selectors of the first type and applies transformation logic to shape it into a more usable model.
I don't write specific tests for the first category, trusting TypeScript to catch simple errors like typos or incorrect property names.
The transformation logic, residing in the projector part of the second category, is what truly requires testing.
There are two primary ways to test these more complex selectors:
- Pass the entire state tree to the selector, which also exercises the logic within child selectors.
- Call the selector's projector method directly with specific inputs, focusing only on the transformation logic itself.
While providing the entire state tree covers more production code, it often results in higher maintenance cost. For that reason, I usually opt for testing the projector method directly.
The test for a selector is quite basic: you invoke the projector with a defined input and assert on the output it returns.
import { createSelector } from '@ngrx/store';
import { fromRouter } from '../routing';
import { customersFeature } from './reducer.ts';
// the selector reads the current customer id from the router url
// based on the customer id, the customer and the customer's invoices are retrieved
// the selector returns the current customer with the linked invoices
export const selectCurrentCustomerWithInvoices = createSelector(
fromRouter.selectCustomerId,
customersFeature.selectCustomers,
customersFeature.selectInvoices,
(customerId, customers, invoices) => {
if (!customerId) {
return null;
}
const customer = customers[customerId];
const invoicesForCustomer = invoices[customerId];
return {
customer,
invoices: invoicesForCustomer,
};
},
);
import { selectCurrentCustomerWithInvoices } from '../selectors';
it('selects the current customer with linked invoices', () => {
const customer = newCustomer();
const invoices = [newInvoice(), newInvoice()];
const result = selectCurrentCustomerWithInvoices.projector(customer.id, {
customers: {
[customer.id]: customer,
},
invoices: {
[customer.id]: invoices,
},
});
expect(result).toEqual({ customer, invoices });
});
function newCustomer(): Customer {
return { id: '1', name: 'Jane' };
}
function newInvoice(): Invoice {
return { id: '1', total: 100.3 };
}
Effects
Effects handle the side-effects of the application. The typical example is an asynchronous operation, say, an effect that performs an HTTP request.
Testing NgRx effects gets interesting precisely because they introduce dependencies — the first time we've had to deal with them in this project.
For simple and fast effect tests, I favor a manual approach: instead of relying on Angular's dependency container via TestBed, I instantiate the effect class directly and supply all dependencies myself. Since I'm providing the dependencies, I can also mock them easily. The code samples below use Jest to create mocks.
I prefer not to use marble diagrams for most of my effect tests. The goal is to keep them straightforward and to test the right thing: the intended flow. We don't need to test implementation details like the choice of higher-order mapping operator or the use of time-based operators (e.g., delay, throttle). Those behaviors are already well-tested within the RxJS library itself.
Effect tests can get tricky, so let's build up from a basic example and then move on to more complex scenarios.
Effects that use Actions and Services
The simplest common case is when an effect reacts to an action by making an HTTP request. The effect class has two dependencies: the Actions stream and a service that wraps HTTP.
import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { customersApiActions, customerPageActions } from '../actions';
import { CustomerService } from './customer.service';
@Injectable()
export class CustomerEffects {
// the effect initiates a request to the customers service when the page is entered
// depending on the response, the effect dispatches a success or failure action
fetch$ = createEffect(() => {
return this.actions$.pipe(
ofType(customerPageActions.enter),
switchMap((action) =>
this.customerService.getById(action.customerId).pipe(
map((customer) => customersApiActions.fetchCustomerSuccess({ customer })),
catchError(() => of(customersApiActions.fetchCustomerError({ customerId }))),
),
),
);
});
constructor(private actions$: Actions, private customerService: CustomerService) {}
}
To test the fetch$ effect, you'll need to create its instance, which requires the Actions stream and a CustomerService.
Since CustomerService is our own code, it's straightforward to create a mock for it. This prevents the effect from hitting the network.
Mocking Actions is trickier. Being a typed observable, it doesn't lend itself to simple mocking. A plain observable won't do either, because you need to push actions during the test to trigger the effect.
A Subject is a good starting point, but it needs to be typed to only accept actions: Subject<Action>. While that works, it isn't the most ergonomic. I prefer using the ActionsSubject from @ngrx/store — a type-safe Actions subject — instead.
With that, you can create a new instance of the effect and send it actions. The last thing you need is the effect's output, which means subscribing to it and capturing the emitted actions.
import { ActionsSubject, Action } from '@ngrx/store';
import { CustomersEffects } from '../customers.effects';
import { CustomerService } from '../customer.service';
import { customersApiActions, customerPageActions } from '../actions';
it('fetch$ dispatches a success action', () => {
// 🔦 The Effect Actions stream is created by instantiating a new `ActionsSubject`
const actions = new ActionsSubject();
const effects = new CustomersEffects(actions, newCustomerService());
// 🔦 Subscribe on the effect to catch emitted actions, which are used to assert the effect output
const result: Action[] = [];
effects.fetch$.subscribe((action) => {
result.push(action);
});
const action = customerPageActions.enter({ customerId: '3' });
actions.next(action);
expect(result).toEqual([
customersApiActions.fetchCustomerSuccess(
newCustomer({
id: action.customerId,
}),
),
]);
});
it('fetch$ dispatches an error action on failure', () => {
// 🔦 The actions stream is created by instantiating a new `ActionsSubject`
const actions = new ActionsSubject();
let customerService = newCustomerService();
// 🔦 Service method is test specific
customerService.getById = (customerId: number) => {
return throwError('Yikes.');
};
const effects = new CustomersEffects(actions, customerService());
const result: Action[] = [];
effects.fetch$.subscribe((action) => {
result.push(action);
});
const action = customerPageActions.enter({ customerId: '3' });
actions.next(action);
expect(result).toEqual([
customersApiActions.fetchCustomerError({
customerId: action.customerId,
}),
]);
});
function newCustomer({ id = '1' } = {}): Customer {
return { id, name: 'Jane' };
}
// 🔦 Service instances are mocked to prevent that HTTP requests are made
function newCustomerService(): CustomerService {
return {
getById: (customerId: number) => {
return of(newCustomer({ id: customerId }));
},
};
}
Effect tests rewritten with observer-spy
The previous tests have a couple of weaknesses.
One is minor: you have to write some boilerplate to catch the emitted actions in each test. That's avoidable with a small utility.
The larger problem is timing. The test's runtime depends on how long the effect takes to finish. This is especially painful for effects that rely on time-based operators. The test could become slow, or worse, flaky if it times out.
The observer-spy library, created by Shai Reznik, addresses this. With it, you can subscribe to a stream, flush pending tasks, and then inspect the emitted values.
Using observer-spy requires these adjustments to your test:
- subscribe to the effect with
subscribeSpyTo - if the test depends on timing, wrap the test callback with the
fakeTimefunction - also for time-sensitive tests, invoke the
flushfunction to fast-forward time and process all pending jobs - use the
getValuesfunction on the subscribed spy to verify the emitted actions
import { subscribeSpyTo, fakeTime } from '@hirez_io/observer-spy';
import { ActionsSubject, Action } from '@ngrx/store';
import { throwError } from 'rxjs';
import { CustomerService } from '../customer.service';
import { CustomersEffects } from '../effects';
import { customersApiActions, customerPageActions } from '../actions';
it(
'fetch$ dispatches success action',
fakeTime((flush) => {
const actions = new ActionsSubject();
const effects = new CustomersEffects(actions, newCustomerService());
const observerSpy = subscribeSpyTo(effects.fetch$);
const action = customerPageActions.enter({ customerId: '3' });
actions.next(action);
flush();
expect(observerSpy.getValues()).toEqual([
customersApiActions.fetchCustomerSuccess(
newCustomer({
id: action.customerId,
}),
),
]);
}),
);
function newCustomer({ id = '1' } = {}): Customer {
return { id, name: 'Jane' };
}
function newCustomerService(): CustomerService {
return {
getById: (customerId: number) => {
return of(newCustomer({ id: customerId }));
},
};
}
Effect tests and fake timers
If you'd rather not add a library just for testing, fake timers are the alternative. This is a framework-agnostic technique; the examples use Jest fake timers.
The tests look similar to the "default" structure, but you gain the ability to control time. With fake timers, you can move time forward for all pending tasks. This is handy in cases where you can't subscribe to a stream, like in a component, as opposed to observer-spy which requires subscribing to an Observable to flush tasks.
Fake timers give you three ways to advance time:
-
advanceTimersByTime: advances the clock by a set number of milliseconds. -
runOnlyPendingTimers: runs current tasks until they finish. -
runAllTimers: runs every task until all of them are done.
One recommendation stands out: prefer runOnlyPendingTimers or runAllTimers over advanceTimersByTime. Waiting for tasks to complete, instead of assuming a specific duration, makes your tests more robust to changes in the timing configuration.
afterEach(() => {
// don't forget to reset the timers
jest.useRealTimers();
});
it('fetch$ dispatches success action with fake timers', () => {
jest.useFakeTimers();
const actions = new ActionsSubject();
const effects = new WerknemersEffects(actions, getMockStore(), newWerknemerService());
const result: Action[] = [];
effects.fetch$.subscribe((action) => {
result.push(action);
});
const action = werknemerActions.missingWerknemerOpened({ werknemerId: 3 });
actions.next(action);
jest.advanceTimersByTime(10_000);
// 🔦 to make tests less brittle, wait for the task to finish with `runOnlyPendingTimers` or `runOnlyPendingTimers` instead of advancing the time with `advanceTimersByTime`.
// This makes sure that the test isn't impacted when the duration is modified.
jest.runOnlyPendingTimers();
expect(result).toEqual([
werknemerActions.fetchWerknemerSuccess({
werknemer: newWerknemer({ id: action.werknemerId }),
}),
]);
});
Effects that don't dispatch actions
So far, every effect we've seen ends with dispatching an action. But some effects don't; they use the dispatch: false option to perform a side-effect instead.
Testing these is straightforward — reuse 90% of the existing test structure and change the assertion. Instead of checking for emitted actions, verify the side-effect has occurred.
For instance, the following test confirms that an action leads to a notification being shown.
import { ActionsSubject, Action } from '@ngrx/store';
import { throwError } from 'rxjs';
import { BackgroundEffects } from '../background.effects';
import { NotificationsService } from '../notifications.service';
import { backgroundSocketActions } from '../actions';
it('it shows a notification on done', () => {
const notifications = newNotificationsService();
const actions = new ActionsSubject();
const effects = new BackgroundEffects(actions, notifications);
effects.done$.subscribe();
const action = backgroundSocketActions.done({ message: 'I am a message' });
actions.next(action);
expect(notifications.info).toHaveBeenCalledWith(action.message);
});
function newNotificationsService(): NotificationsService {
return {
success: jest.fn(),
error: jest.fn(),
info: jest.fn(),
};
}
To verify the absence of dispatch (i.e., it's set to false), you can use the getEffectsMetadata method. It returns the configuration for all effects in a class. Then, access the config of the effect you're interested in — in this case, the done$ member.
import { ActionsSubject, Action } from '@ngrx/store';
import { getEffectsMetadata } from '@ngrx/effects';
import { throwError } from 'rxjs';
import { BackgroundEffects } from '../background.effects';
import { NotificationsService } from '../notifications.service';
import { backgroundSocketActions } from '../actions';
it('it shows a notification on done', () => {
const notifications = newNotificationsService();
const actions = new ActionsSubject();
const effects = new BackgroundEffects(actions, notifications);
effects.done$.subscribe();
const action = backgroundSocketActions.done({ message: 'I am a message' });
actions.next(action);
expect(getEffectsMetadata(effects).done$.dispatch).toBe(false);
expect(notifications.info).toHaveBeenCalledWith(action.message);
});
function newNotificationsService(): NotificationsService {
return {
success: jest.fn(),
error: jest.fn(),
info: jest.fn(),
};
}
Effects that use the NgRx Global Store
Starting with NgRx v11, there's a getMockStore method (imported from @ngrx/store/testing) for creating a mock store instance. This helps us stick with manual instantiation for effects that otherwise would need the Angular TestBed to provide the store.
Take an effect that fetches an entity only when it's missing from the store. The effect uses a selector to read the entities from the store. An implementation of this kind is shown in a related post, Start using NgRx Effects for this.
The relevant test uses getMockStore to simulate the ngrx store. This mock store accepts a configuration object where you can set up the selectors used by the effect.
The crucial detail is that by providing a return value, you skip the selector's actual logic and substitute the value directly. Everything else in the test stays the same.
import { ActionsSubject, Action } from '@ngrx/store';
import { getMockStore } from '@ngrx/store/testing';
import { CustomersEffects } from '../customers.effects';
import { CustomerService } from '../customer.service';
import { customersApiActions, customerPageActions } from '../actions';
it('fetch$ dispatches success action', () => {
const actions = new ActionsSubject();
const effects = new CustomersEffects(
actions,
getMockStore({
selectors: [{ selector: selectCustomerIds, value: [1, 3, 4] }],
}),
newCustomerService(),
);
const result: Action[] = []
effects.fetch$.subscribe((action) => {
result.push(action)
})
const existingAction = customerPageActions.enter({ customerId: 1 });
const newAction1 = customerPageActions.enter({ customerId: 2 });
const newAction2 = customerPageActions.enter({ customerId: 5 });
actions.next(existingAction);
actions.next(newAction1);
actions.next(newAction2);
expect(result).toEqual([
customersApiActions.fetchCustomerSuccess(newCustomer({ id: newAction1.customerId })),
customersApiActions.fetchCustomerSuccess(newCustomer({ id: newAction2.customerId })),
]);
});
Effects that use the Angular Router
Manually creating a Router instance is a chore, and the Router API doesn't offer a simple factory method outside the Angular TestBed.
You have a couple of options: build a minimal custom implementation that mocks only the methods you need, or use a helper library that automatically generates spy implementations for all members and methods of a type, like the Router.
In the test below, we need to verify that the window title is updated on navigation. The example uses createMock from the Angular Testing Library (imported from @testing-library/angular/jest-utils) to mock the Title service.
The test also uses createMockWithValues to provide a custom implementation for the router events. This lets us emit new navigation events as needed to trigger the effect, whose implementation you can find in another post, Start using NgRx Effects for this.
Here's the test for updating the window title during navigation:
import { Title } from '@angular/platform-browser';
import { NavigationEnd, Router, RouterEvent } from '@angular/router';
import { createMock, createMockWithValues } from '@testing-library/angular/jest-utils';
import { Subject } from 'rxjs';
import { RoutingEffects } from '../routing.effects';
it('sets the title to the route data title', () => {
const routerEvents = new Subject<RouterEvent>();
const router = createMockWithValues(Router, {
events: routerEvents,
});
const title = createMock(Title);
const effect = new RoutingEffects(
router,
{
firstChild: {
snapshot: {
data: {
title: 'Test Title',
},
},
},
} as any,
title,
);
effect.title$.subscribe()
routerEvents.next(new NavigationEnd(1, '', ''));
expect(title.setTitle).toHaveBeenCalledWith('Test Title');
});
Container Components and the Global Store
Once most of the logic lives outside the component, you're typically left with a lean component that has minimal dependencies to configure during testing. In many applications, components are further split into two groups: containers that connect to the store, and presentational components that purely render props.
This section is concerned with containers, since they're the ones that talk to the NgRx global store. If you'd like to learn more about testing presentational components, I've written about that topic in a previous article, Getting the most value out of your Angular Component Tests.
When it comes to testing container components, there are two primary strategies.
The first treats the component test as an integration test. Here, the actual selectors, reducers, and effects are wired up, while only external service calls are stubbed out. On the surface, this aligns with the "don't test implementation details" principle and therefore seems like the ideal approach. However, I'd steer away from it, since such tests tend to be brittle and painful to set up. You have to configure the store thoroughly, understand every dependency inside out, and keep the state tree in sync.
That's the opposite of what we're aiming for.
The goal is to write tests that assist in building and maintaining the app, not tests that nobody wants to touch because they're so intricate. The maintenance overhead for a test like that can easily surpass the time spent writing new features.
The second strategy is to keep it as a unit test, focusing purely on the component and its interaction with the store. For that, a mocked store is used to ensure that reducers and effects don't actually run.
In my experience, unit testing containers is the most productive compromise, and it still leaves us with solid confidence in the code. Because we have targeted unit tests for reducers, selectors, effects, and containers individually, each test remains easy to understand.
Testing a component requires, for the first time, the Angular TestBed.
Once more, we turn to the Angular Testing Library. Not only does it simplify the setup and the component interaction, but it also nudges you toward writing more user-friendly components.
It's a win-win.
For the store injection, the provideMockStore method (from @ngrx/store/testing) is registered as an Angular provider.
As an illustration, consider a component that renders a customer.
It pulls the customer from the store via the selectCustomerWithOrders selector and shows both the customer and their orders. It also has a refresh button that triggers a customersPageActions.refresh dispatch.
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectCustomerWithOrders } from './selectors';
import { customersPageActions } from './actions';
@Component({
selector: 'app-customer-page',
template: `
<ng-container *ngIf="customer$ | async as customer">
<h2>Customer: {{ customer.name }}</h2>
<button (click)="refresh(customer.id)">Refresh</button>
<table>
<thead>
<tr>
<th>Date</th>
<th>Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let order of customer.orders">
<td>{{ order.date }}</td>
<td>{{ order.amount }}</td>
<td>{{ order.status }}</td>
</tr>
</tbody>
</table>
</ng-container>
`,
})
export class CustomersSearchPageComponent {
customer$ = this.store.select(selectCustomerWithOrders);
constructor(private store: Store) {}
refresh(customerId: string) {
this.store.dispatch(customersPageActions.refresh({ customerId }));
}
}
The test that verifies the customer's name appears on screen is shown below.
The key detail is that we provide a mock store and give the selector a mocked return value. This way, there's no need to configure the full store; we only supply what the component needs, keeping the test readable and concise.
A couple of practices worth highlighting:
🔦 toBeVisible comes from jest-dom, a set of custom Jest matchers.
🔦 SIFERS, a test setup pattern introduced by Moshe Kolodny.
import { provideMockStore } from '@ngrx/store/testing';
import { render, screen } from '@testing-library/angular';
import { selectCustomerWithOrders, CustomerWithOrders } from '../selectors';
import type { CustomerWithOrders } from '../selectors';
import { customersPageActions } from '../actions';
it('renders the customer with her orders', async () => {
const customer = newCustomer();
customer.orders = [
{ date: '2020-01-01', amount: 100, status: 'canceled' },
{ date: '2020-01-02', amount: 120, status: 'shipped' },
];
// 🔦 Testing With SIFERS by Moshe Kolodny https://medium.com/@kolodny/testing-with-sifers-c9d6bb5b36
await setup(customer);
// 🔦 toBeVisible is a custom jest matcher from jest-dom
expect(
screen.getByRole('heading', {
name: new RegExp(customer.name, 'i'),
}),
).toBeVisible();
// the table header is included
expect(screen.getAllByRole('row')).toHaveLength(3);
screen.getByRole('cell', {
name: customer.orders[0].date,
});
screen.getByRole('cell', {
name: customer.orders[0].amount,
});
screen.getByRole('cell', {
name: customer.orders[0].status,
});
});
// 🔦 Testing With SIFERS by Moshe Kolodny https://medium.com/@kolodny/testing-with-sifers-c9d6bb5b362
async function setup(customer: CustomerWithOrders) {
await render('<app-customer-page></app-customer-page>', {
imports: [CustomerPageModule],
providers: [
provideMockStore({
selectors: [{ selector: selectCustomerWithOrders, value: customer }],
}),
],
});
}
function newCustomer(): CustomerWithOrders {
return {
id: '1',
name: 'Jane',
orders: [],
};
}
The example above checks that the component renders its content accurately.
Now we'll look at how to verify that an action gets dispatched, specifically when the refresh button is clicked.
For that assertion, we set a spy on the store's dispatch method. That spy is then used in the expectation to confirm that the refresh action was indeed sent.
import { provideMockStore } from '@ngrx/store/testing';
import { render, screen } from '@testing-library/angular';
import { selectCustomerWithOrders, CustomerWithOrders } from '../selectors';
import type { CustomerWithOrders } from '../selectors';
import { customersPageActions } from '../actions';
it('renders the customer name', async () => {
const customer = newCustomer();
customer.orders = [
{ date: '2020-01-01', amount: 100, status: 'canceled' },
{ date: '2020-01-02', amount: 120, status: 'shipped' },
];
// 🔦 Testing With SIFERS by Moshe Kolodny https://medium.com/@kolodny/testing-with-sifers-c9d6bb5b362
const { dispatchSpy } = await setup(customer);
// 🔦 toBeVisible is a custom jest matcher from jest-dom
expect(
screen.getByRole('heading', {
name: new RegExp(customer.name, 'i'),
}),
).toBeVisible();
// the table header is included
expect(screen.getAllByRole('row')).toHaveLength(3);
screen.getByRole('cell', {
name: customer.orders[0].date,
});
screen.getByRole('cell', {
name: customer.orders[0].amount,
});
screen.getByRole('cell', {
name: customer.orders[0].status,
});
userEvent.click(
screen.getByRole('button', {
name: /refresh/i,
}),
);
expect(dispatchSpy).toHaveBeenCalledWith(
customersPageActions.refresh({ customerId: customer.id }),
);
});
// 🔦 Testing With SIFERS by Moshe Kolodny https://medium.com/@kolodny/testing-with-sifers-c9d6bb5b362
async function setup(customer: CustomerWithOrders) {
await render('<app-customer-page></app-customer-page>', {
imports: [CustomerPageModule],
providers: [
provideMockStore({
selectors: [{ selector: selectCustomerWithOrders, value: customer }],
}),
],
});
const store = TestBed.inject(MockStore);
store.dispatch = jest.fn();
return { dispatchSpy: store.dispatch };
}
function newCustomer(): CustomerWithOrders {
return {
id: '1',
name: 'Jane',
orders: [],
};
}
Component Store in Tests
Unlike the global NgRx store, a component store is tightly bound to its component. For that reason, I tend to treat the component store as an internal detail, and I rarely stub it out during tests. Since the test exercises the real component store, its own dependencies—like services that reach out to the outside world—need to be mocked instead.
Take the CustomersSearchStore, which is consumed by CustomersSearchPageComponent. The store manages the customer list state and issues an HTTP call to load customers, while the component reads from the store to display them.
import { Injectable } from '@angular/core';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { Observable, delay, switchMap } from 'rxjs';
import { CustomersService } from './services';
import { Customer } from './models';
export interface CustomersSearchState {
customers: Customer[];
}
@Injectable()
export class CustomersSearchStore extends ComponentStore<CustomersSearchState> {
constructor(private readonly customersService: CustomersService) {
super({ customers: [] });
}
readonly customers$ = this.select((state) => state.customers);
setCustomers(customers: Customer[]) {
this.patchState({ customers });
}
clearCustomers() {
this.patchState({ customers: [] });
}
readonly search = this.effect((trigger$: Observable<string>) => {
return trigger$.pipe(
delay(1000),
switchMap((query) =>
this.customersService.search(query).pipe(
tapResponse(
(customers) => this.setCustomers(customers),
() => this.clearCustomers(),
),
),
),
);
});
}
import { Component } from '@angular/core';
import { CustomersSearchStore } from './customers-search.store';
@Component({
template: `
<input type="search" #query />
<button (click)="search(query.value)">Search</button>
<a *ngFor="let customer of customers$ | async" [routerLink]="['customer', customer.id]">
{{ customer.name }}
</a>
`,
providers: [CustomersSearchStore],
})
export class CustomersSearchPageComponent {
customers$ = this.customersStore.customers$;
constructor(private readonly customersStore: CustomersSearchStore) {}
search(query: string) {
this.customersStore.search(query);
}
}
To illustrate the distinction between integration and unit testing, we'll apply both approaches to the same component.
Integration Tests
An integration test checks that the component and its component store work together as expected. If you've been following the earlier sections, the structure here will look familiar.
The component test relies on Angular Testing Library. In the setup, a mock of the CustomersService is provided, since it's a dependency of the component store. From that point on, the test simulates a user interaction with the store and confirms the correct output on screen. Given the debounce on the search query, Jest fake timers are used to fast-forward time.
Such tests are usually longer and verify multiple expectations at once. That's acceptable—and even encouraged—when working with the Angular Testing Library.
import { RouterTestingModule } from '@angular/router/testing';
import { render, screen } from '@testing-library/angular';
import { provideMockWithValues } from '@testing-library/angular/jest-utils';
import userEvent from '@testing-library/user-event';
import { of } from 'rxjs';
import { CustomersSearchPageComponent } from '../customers-search.component';
import { Customer } from '../models';
import { CustomersService } from '../services';
afterEach(() => {
jest.useRealTimers();
});
it('fires a search and renders the retrieved customers', async () => {
jest.useFakeTimers();
await setup();
expect(screen.queryByRole('link')).not.toBeInTheDocument();
userEvent.type(screen.getByRole('searchbox'), 'query');
userEvent.click(
screen.getByRole('button', {
name: /search/i,
}),
);
jest.runOnlyPendingTimers();
const link = await screen.findByRole('link', {
name: /query/i,
});
expect(link).toHaveAttribute('href', '/customer/1');
});
async function setup() {
await render(CustomersSearchPageComponent, {
imports: [RouterTestingModule.withRoutes([])],
providers: [
provideMockWithValues(CustomersService, {
search: jest.fn((query) => {
return of([newCustomer(query)]);
}),
}),
],
});
}
function newCustomer(name = 'customer'): Customer {
return {
id: '1',
name,
};
}
Unit Tests
When a component store is elaborate or pulls in several dependencies, it can be more practical to test the component store and the component on their own. This approach makes it easier to cover specific edge cases. The suite also tends to execute faster, because the component template doesn't need to be rendered for the bulk of the specs—those live in the component store tests.
In this setup, only a handful of component tests touch the component store. Their purpose is to confirm that the component invokes the store correctly and reacts to its state changes.
Component Store Unit Tests
Expect a steady stream of small, focused tests here, each one validating a single method on the component store. Frequently, these tests update the state and then inspect that the state has taken the expected shape.
import { createMockWithValues } from '@testing-library/angular/jest-utils';
import { of, throwError } from 'rxjs';
import { Customer, CustomersSearchStore } from '../customers-search.store';
import { CustomersService } from '../services';
afterEach(() => {
jest.useRealTimers();
});
it('initializes with no customers', async () => {
const { customers } = setup();
expect(customers).toHaveLength(0);
});
it('search fills the state with customers', () => {
jest.useFakeTimers();
const { store, customers, service } = setup();
const query = 'john';
store.search(query);
jest.runOnlyPendingTimers();
expect(service.search).toHaveBeenCalledWith(query);
expect(customers).toHaveLength(1);
});
it('search error empties the state', () => {
jest.useFakeTimers();
const { store, customers } = setup(() => throwError('Yikes.'));
store.setState({ customers: [newCustomer()] });
store.search('john');
jest.runOnlyPendingTimers();
expect(customers).toHaveLength(0);
});
it('clearCustomers empties the state', () => {
const { store, customers } = setup();
store.setState({ customers: [newCustomer()] });
store.clearCustomers();
expect(customers).toHaveLength(0);
});
function setup(customersSearch = (query: string) => of([newCustomer(query)])) {
const service = createMockWithValues(CustomersService, {
search: jest.fn(customersSearch),
});
const store = new CustomersSearchStore(service);
let customers: Customer[] = [];
store.customers$.subscribe((state) => {
customers.length = 0;
customers.push(...state);
});
return { store, customers, service };
}
function newCustomer(name = 'customer'): Customer {
return {
id: '1',
name,
};
}
Component Unit Tests with a Mocked Component Store
Compared to the component store tests, only a few component specs depend on the component store. These specs are also shorter than the integration-style tests seen earlier. During setup, the component store is replaced with a mock. Since the store is registered at the component level, the mocked instance goes into the componentProviders array.
These tests fall into two buckets. The first bucket verifies that the view accurately reflects the store's state. The test assigns a predefined return value to the store's selectors, renders the component, and then checks the resulting output.
The second bucket checks that user actions trigger the right store methods. Spies are placed on the component store's methods, and the test confirms that the expected method fires after a given interaction.
import { RouterTestingModule } from '@angular/router/testing';
import { render, screen } from '@testing-library/angular';
import { createMockWithValues } from '@testing-library/angular/jest-utils';
import userEvent from '@testing-library/user-event';
import { of } from 'rxjs';
import { CustomersSearchPageComponent } from '../customers-search.component';
import { Customer, CustomersSearchStore } from '../customers-search.store';
it('renders the customers', async () => {
await setup();
const link = await screen.findByRole('link', {
name: /customer/i,
});
expect(link).toHaveAttribute('href', '/customer/1');
});
it('invokes the search method', async () => {
const { store } = await setup();
const query = 'john';
userEvent.type(screen.getByRole('searchbox'), query);
userEvent.click(
screen.getByRole('button', {
name: /search/i,
}),
);
expect(store.search).toHaveBeenCalledWith(query);
});
async function setup() {
const store = createMockWithValues(CustomersSearchStore, {
customers$: of([newCustomer()]),
search: jest.fn(),
});
await render(CustomersSearchPageComponent, {
imports: [RouterTestingModule.withRoutes([])],
componentProviders: [
{
provide: CustomersSearchStore,
useValue: store,
},
],
});
return { store };
}
function newCustomer(): Customer {
return {
id: '1',
name: 'name',
};
}
Wrapping Up
Testing an Angular app doesn't have to feel like a burden. When tests are well-crafted, they serve to confirm the app is functioning properly, without slowing you down as you add or change features.
In my view, the sweet spot is a test that mocks sparingly and keeps the setup lean. That kind of test tends to stay maintainable over time.
The tests above have deliberately avoided the Angular TestBed wherever possible, to keep things straightforward.
Reducers are tested by passing in a known state and an action, then asserting that the resulting state is correct.
Selectors with logic are tested via the projector. Instead of feeding in the whole state tree, the projector is invoked with the output of its child selectors, and the return value is checked against expectations.
Effect tests skip the Angular TestBed entirely. The effect is instantiated directly, its dependencies are mocked, and the effect's action stream is subscribed to for inspection. A new action is pushed onto the ActionsSubject to trigger the effect.
Components wired to the global store are tested with the Angular Testing Library. In these component tests, a mocked store stands in for the real one.
Components built around a component store get both unit and integration tests. My preference leans toward integration tests, but when those become unwieldy, I switch to unit tests. Integration tests run the real store with mocked dependencies. Unit tests split the work: the component store is tested on its own, and a separate round of component tests runs against a mocked store instance.
Happy testing!
Follow me on Twitter at @tim_deschryver | Subscribe to the Newsletter | Originally published on timdeschryver.dev.
