Learn how to unit test Angular components using NgRx with Jest.
Download
In this post I’ll be referencing code in a very basic demo application that uses NgRx. 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
We’ll be using Jest to write unit tests for an Angular application that uses NgRx for state management. It’s much faster than Karma (even with headless Chrome) and uses a Jasmine-like API.
If you’re new to Jest, check out my post on using Jest with Angular.
jasmine-marbles
We’ll be using the jasmine-marbles module in order to mock observables.
Here are some links to more information about marble testing with NgRx:
In summary, we can use marble-diagram-like strings to describe an observable stream over time. This enables us to synchronously test asynchronous observable streams.
There are two primary functions that we will be using:
hot()
creates a hot observable stream.cold()
creates a cold observable stream.
We’ll use these function to assert that the outcome of specific actions in our application result in the expected hot or cold observable stream that we describe.
Marble testing uses a string to describe the observable stream:
-
a dash represents a frame of virtual time passing, which is 10 milliseconds, but that’s not really that important.a-z0-9
any alphanumeric character represents a value. We can use the second argument to thehot()
andcold()
functions to specify the values that each marker represents.()
paranthesis group together values into a single frame.^
the carrot represents the start of a subscription in a cold observable stream.|
the pipe represents the completion notification.#
the pound sign (or hash symbol) represents the error notification.
Container vs Presentation
In the sample application I am employing the component architecture of separating “container” components from “presentation” components. In summary:
- Container components retrieve data and mutate the state of your application.
- Presentation components are highly reusable components that do not rely on the state of the application. They rely on data that is input into the component and emit events as output.
Index Component
Let’s take a look at testing a container component in an application that is using NgRx.
First, let’s get things set up in the src/app/users/containers/index.component.spec.ts file:
describe('IndexComponent', () => {
let component: IndexComponent;
let fixture: ComponentFixture<IndexComponent>;
let unsubscribe = new Subject<void>();
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [IndexComponent, UserListComponent],
imports: [RouterTestingModule],
providers: [
{
provide: Store,
useValue: {
dispatch: jest.fn(),
pipe: jest.fn()
}
}
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IndexComponent);
component = fixture.componentInstance;
});
afterEach(() => {
unsubscribe.next();
unsubscribe.complete();
});
});
Let’s review the setup for the tests:
- As you can see, Jest uses Jasmine-like methods for setting up the test suite using the
describe()
, along with global methods likebeforeEach()
andafterEach()
. - First, we set up the
TestBed
in the firstbeforeEach()
method. - We add the child components
IndexComponent
andUserListComponent
to thedeclaration
array just like we would add in the module. - We are importing the
RouterTestingModule
to mock the<router-outlet>
element. - We are mocking out an object for the
Store
, with two properties:dispatch()
andpipe()
. - Then, in the second
beforeEach()
method we create the componentfixture
and store a reference to the componentinstance
. - Finally, we use the
afterEach()
method to tear down any subscriptions in our tests using thetakeUntil
operator in RxJS.
The standard “it should create” test follows:
it('should create', () => {
expect(component).toBeTruthy();
});
Nothing special there. This is straight out-of-the box when we generated the component using the Angular CLI.
Let’s take a look at testing the ngOnInit()
method:
ngOnInit() {
this.store.dispatch(new LoadUsers());
this.users = this.store.pipe(select(selectAllUsers));
}
We are simply dispatching the LoadUsers
action to the store, and also using the selectAllUsers
selector to get an array of users that is in the store, which happens to be an @ngrx/entity collection of User
objects.
Let’s test it:
describe('ngOnInit()', () => {
it('should dispatch an the LoadUsers action in ngOnInit lifecycle', () => {
const action = new LoadUsers();
const store = TestBed.get(Store);
const spy = jest.spyOn(store, 'dispatch');
fixture.detectChanges();
expect(spy).toHaveBeenCalledWith(action);
});
it('should selectAllUsers', () => {
const store = TestBed.get(Store);
const users = generateUsers();
store.pipe = jest.fn(() => hot('-a', { a: users }));
fixture.detectChanges();
const expected = cold('-a', { a: users });
expect(component.users).toBeObservable(expected);
});
});
We want to test both the dispatch()
and pipe()
methods that are invoked in the ngOnInit()
method.
First, we create a test for dispatching the LoadUsers
action:
- First, we new up the
LoadUsers
action class. - Then, we get a reference to the
Store
using theTestBed.get()
method. - And then we set up a spy on the
dispatch()
method using thejest.spyOn()
function. - We then invoke the
ngOnInit()
method by triggering change detection using thedetectChanges()
method on the fixture. - Finally, we are expecting the
dispatch()
method to have been called with theLoadUsers
action.
Then, we create a test for selecting the users from the state object:
- First, we get a reference to the
Store
using theTestBed.get()
method. - Next, I use an exported
generateUsers()
function to generate a fake array ofUser
objects. I am using the faker library to generate some fake data. - Next, we are mocking the
pipe()
method on thestore
instance. The mock function returns a newhot()
observable stream. The observable stream is composed of a frame (virtual “time” has passed) followed by a value,a
, which is theusers
array.
Finally, we should write a unit test to validate the users
property within our component is a cold observable of an array of User
objects:
describe('users', () => {
it('should be an observable of an array of user objects', (done) => {
const users = generateUsers();
const store = TestBed.get(Store);
store.pipe = jest.fn(() => cold('-a|', { a: users }));
fixture.detectChanges();
component.users.subscribe((componentUsers) => {
expect(componentUsers).toEqual(users);
done();
});
getTestScheduler().flush();
});
});
In this test:
- First, we generate an array of fake
User
objects using thegenerateUsers()
function. - We get a reference to the
Store
object that was injected into our component using theTestBed.get()
method. - We want to mock out the
pipe
method to return the cold observable stream ofUser
objects. We are creating a cold observable because theHttpClient
returns a cold observable. - We then invoke the
ngOnInit()
method by triggering change detection using thedetectChanges()
method on the fixture. - Aftering invoking the
ngOnInit()
method in ourcomponent
, we cansubscribe()
to theusers
property, expecting that thelast()
notification is the array of users. - Note that we use the
done()
function provided by Jest to signal the completion of asynchronous tests. - Finally, we need to
flush()
the tasks in the test scheduler. This will trigger the cold observable to emit the notification.
User List Component
Now that we have tested our container IndexComponent
class, let’s look at an example of testing the UserListComponent
.
This component is a child of the IndexComponent
and is designed to be a presentation component that is stateless.
Let’s look at the component template in src/app/users/components/user-list/user-list.component.html:
<ul>
<li *ngFor="let user of users" trackBy="user.id">
<a href="javascript:void(0)" (click)="selectUser.emit(user)"
>{{user.firstName}} {{user.lastName}}</a
>
</li>
</ul>
As you can see, we are simply rendering an unordered list of users. Nothing fancy here.
First, we want to test that the list of users is rendered in src/app/users/components/user-list/user-list.component.spec.ts:
it('should display an unordered list of heroes', () => {
const ulDebugEl = fixture.debugElement.query(By.css('ul'));
const ulEl = ulDebugEl.nativeElement as HTMLUListElement;
component.users = users;
fixture.detectChanges();
expect(ulEl.childElementCount).toBe(users.length);
const firstLi = ulEl.querySelector('li:first-child');
expect(firstLi.textContent).toEqual(
`${users[0].firstName} ${users[0].lastName}`
);
});
This test is pretty straight forward:
- First, we get reference to the
DebugElement
for the unordered list element. - Then, we get reference to the
HTMLUListElement
object that is thenativeElement
property on theDebugElement
. Note that we have to cast the type to explicitly instruct TypeScript that the DOM element is of typeHTMLUListElement
. - We then set the
users
property in the component. In this case I have a an array ofUser
objects that is stored in a function-block variable namedusers
that is result of invoking thegenerateUsers()
function, which uses faker to generate fake data. - Next, we trigger the change detection in Angular using the
detectChanges()
method on the fixture. - We
expect()
that thechildElementCount
of the unordered list should match the length of the array of users. - We further obtain the first list item element and expect that the
textContent
equals the rendered first name and last name of the first user.
Now, let’s assert that the EventEmitter
emits a user that is clicked:
it('should select a user when clicked', () => {
const user = users[0];
component.users = users;
fixture.detectChanges();
const anchorDebugEl = fixture.debugElement.query(
By.css('ul > li:first-child > a')
);
let selectedUser: User;
component.selectUser.subscribe(user => (selectedUser = user));
anchorDebugEl.triggerEventHandler('click', user);
expect(selectedUser).toEqual(user);
});
Let’s review the test:
- First, we store a reference to the first
user
(which we will assert is the user that is clicked/touched). - Like in our previous test, we set the
users
property to the array ofUser
objects that we generated using faker. - We then trigger change detection in Angular.
- Using the
DebugElement
we obtain a reference to the<a>
element that is the direct descendent of the first list item in the list. - We subscribe to the
EventEmitter
for theselectedUser
output event. - Using the
triggerEventHandler()
method we can trigger theclick
event on the first user, supplying theuser
event object. - Finally, we
expect()
that theselectedUser
is equal to theuser
object that was triggered in the click event.
Run Tests
It’s a good time to stop and run our tests to be sure that everything is passing:
$ npm test
Add User Component
Ok, now that we have tests for our IndexComponent
and it’s child UserListComponent
, let’s take a look at testing the AddComponent
.
Here is the template in src/app/users/containers/add/add.component.html:
<app-user-form (userChange)="onUserChange($event)"></app-user-form>
We will be using the UserFormComponent
to display a form for adding a new user, attaching an @Output()
binding for the userChange
, invoking the onUserChange()
method when a value is emitted.
Within the AddComponent
we define the onUserChange()
method in src/app/users/containers/add/add.component.ts:
export class AddComponent {
constructor(private store: Store<fromRoot.State>) {}
onUserChange(user: User) {
this.store.dispatch(new AddUser({ user: user }));
}
}
In the onUserChange()
method we dispatch()
the AddUser
action to the store.
Ok, let’s test that the AddUser
action is dispatched when the onUserChange
emits in src/app/users/containers/add/add.component.spec.ts:
it('should dispatch the AddUser action when onUserChange is invoked', () => {
const user = generateUser();
const action = new AddUser({ user });
const spy = jest.spyOn(store, 'dispatch');
fixture.detectChanges();
component.onUserChange(user);
expect(spy).toHaveBeenCalledWith(action);
});
- First, we use the
generateUser()
function that is imported from the model to create a fakeUser
object using faker. - We new up the
AddUser
action, specifying thepayload
that is theuser
object. - Then, create a spy on the
dispatch()
method in the store usingjest.spyOn()
. - We then trigger change detection and invoke the
onUserChange()
method with theuser
object. - Finally, we assert that the
dispatch()
method is invoked with theAddUser
action.
Edit User Component
Along with the ability to add a user, we also want to be able to edit an existing user.
The EditComponent
template is very similar to the AddComponent
, with the addition of the user
input binding:
<app-user-form
[user]="user$ | async"
(userChange)="onUserChange($event)"
></app-user-form>
The EditComponent
is routed with an id
parameter in order to specify the user to edit.
We’ll use the id
parameter to dispatch two actions: SelectUser
and LoadUser
.
Here is the EditComponent
source in src/app/users/containers/edit/edit.component.ts:
export class EditComponent implements OnInit {
user$: Observable<User>;
constructor(
private activatedRoute: ActivatedRoute,
private store: Store<fromRoot.State>
) {}
ngOnInit() {
const PARAM_ID = 'id';
this.user$ = this.activatedRoute.paramMap.pipe(
filter(paramMap => paramMap.has(PARAM_ID)),
map(paramMap => paramMap.get(PARAM_ID)),
tap(id => {
this.store.dispatch(new SelectUser({ id: +id }));
this.store.dispatch(new LoadUser({ id: +id }));
}),
switchMap(id => this.store.pipe(select(selectSelectedUser)))
);
}
onUserChange(user: User) {
this.store.dispatch(new UpdateUser({ user: user }));
}
}
Before we can start testing our component we need to wire up the TestBed
:
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [EditComponent, UserFormComponent],
imports: [FormsModule, RouterTestingModule, ReactiveFormsModule],
providers: [
{
provide: ActivatedRoute,
useValue: {
paramMap: new BehaviorSubject(
convertToParamMap({
id: user.id,
}),
),
},
},
{
provide: Store,
useValue: {
dispatch: jest.fn(),
pipe: jest.fn(() => hot('-a', { a: user })),
},
},
],
}).compileComponents();
}));
Let’s review the beforeEach()
method:
- First, we specify the
EditComponent
andUserFormComponent
as we would in the module. - We add the necessary
imports
for working with reactive forms and the router. - In the
providers
array we need to mock out theActivatedRoute
andStore
. We use aBehaviorSubject
to mock theparamMap
property on theActivatedRoute
, specifying theid
of the fake user. And we mock out both thedispatch()
andpipe()
methods on theStore
. Note that thepipe()
mock returns a hot observable which emits the user object after a single frame.
With our TestBed
ready to go, let’s test the ngOnInit()
lifecycle method:
describe('ngOnInit', () => {
it('should dispatch SelectUser action for specified id parameter', () => {
const action = new SelectUser({ id: user.id });
const spy = jest.spyOn(store, 'dispatch');
fixture.detectChanges();
expect(spy).toHaveBeenCalledWith(action);
});
it('should dispatch LoadUser action for specified id parameter', () => {
const action = new LoadUser({ id: user.id });
const spy = jest.spyOn(store, 'dispatch');
fixture.detectChanges();
expect(spy).toHaveBeenCalledWith(action);
});
});
Our “ngOnInit” test suite contains two tests: asserting the dispatching of the SelectUser
and LoadUser
actions.
Next, let’s test the onUserChange()
method that is invoked when the userChange
EventEmitter
emits the updated user object from the <user-form>
component.
describe('onUserChange', () => {
it('should dispatch the UpdateUser action when onUserChange is invoked', () => {
const user = generateUser();
const action = new UpdateUser({ user });
const spy = jest.spyOn(store, 'dispatch');
fixture.detectChanges();
component.onUserChange(user);
expect(spy).toHaveBeenCalledWith(action);
});
});
This is identical to the test for the AddComponent
.
User Form Component
With both the AddComponent
and EditComponent
tested, the next logical step is to create a unit test for the UserFormComponent
.
Here is the template in src/app/users/components/user-form/user-form.component.html:
<form [formGroup]="form">
<input type="text" placeholder="First Name" formControlName="firstName" />
<input type="text" placeholder="Last Name" formControlName="lastName" />
<button (click)="onSave()">Save</button>
</form>
I am using the ReactiveFormsModule
to create a simple form for the user that contains two inputs: first name and last name.
When the “Save” button in clicked, the onSave()
method in the component is invoked.
In the UserFormComponent
we define the onSave()
method:
onSave() {
if (this.form.invalid) {
return;
}
this.userChange.emit({
...this.user,
...this.form.value
});
}
Fist, let’s assert that the form values are patched when the ngOnChanges()
lifecycle method is invoked:
it('should patch values into the form', () => {
const user = generateUser();
component.ngOnChanges({
user: new SimpleChange(null, user, true),
});
expect(component.form.value).toEqual({
firstName: user.firstName,
lastName: user.lastName,
});
});
In this test we:
- Generate a fake user
- Invoke the
ngOnChanges()
method, supplying a newSimpleChange
object for auser
. - Expect the
value
of theFormGroup
to include the user’s first and last name.
Then, let’s assert that the userChange
output event is emitted when the form is submitted:
it('should emit the userChange event when submitted', () => {
const user = generateUser();
const firstName = 'Brian';
const firstNameDebugEl = fixture.debugElement.query(
By.css('input[formControlName="firstName"]')
);
const firstNameEl = firstNameDebugEl.nativeElement as HTMLInputElement;
const buttonDebugEl = fixture.debugElement.query(By.css('button'));
fixture.detectChanges();
let updatedUser: User;
component.userChange.subscribe(user => (updatedUser = user));
component.user = user;
component.ngOnChanges({
user: new SimpleChange(null, user, true)
});
firstNameEl.value = firstName;
firstNameEl.dispatchEvent(newEvent('input'));
buttonDebugEl.triggerEventHandler('click', null);
expect(updatedUser).toEqual({
...user,
firstName
});
});
In order to test our component we need to:
- Provide a fake user that is input into the component.
- Trigger the
ngOnChanges()
lifecycle method, which will populate the form using thepatchValue()
method on theFormGroup
. - Subscribe to the
userChange
output. - Update the value of the
firstName
input. - Trigger the
click
event on the submit button.
After populating the form with the initial user data, updating the first name input value, and subscribing to the userChange
EventEmitter
we can expect()
that the updated user
that is emitted equals the user that was provided, with the update first name value.
Conclusion
Unit testing is extremely value, and using a test runner like Jest makes the experience enjoyable with fast iterations.
A big thank you to the NgRx community and all of the documentation on using Rx Marbles for unit testing observables and asynchronous events in Angular. 👏😀👍