Testing Actions page
Learn how to write unit tests for NgRx Actions.
Quick Start: You can checkout this branch to get your codebase ready to work on this section.
Overview
Verify
LoginActions.logout
Action dispatches when callingDashboardComponent
’slogout()
.Verify
LoginActions.login
Action dispatches with form payload when callingLoginComponent
’slogin()
.
Running Tests
To run unit tests in your project, you can either use the test
npm script, or the ng test
command:
npm run test
# or
ng test --watch
The --watch
switch will rerun your tests whenever a code file changes. You can skip it to just run all tests once.
Introduction
Each implementation section will be paired with a testing section. These testing sections will go over the basics of how to test the implementation and use of NgRx. Before continuing, you should have an intermediate understanding of the following:
Angular TestBeds - Although
TestBeds
aren’t required for testing Angular applications, and there are ways to test NgRx without a TestBed, we useTestBeds
throughout this course.Jasmine Unit Tests - Throughout this course, we will be using Jasmine to test our application. Although the syntax will differ slightly between different testing tools such as Mocha and Jest, the concepts used throughout this course will apply to whatever solution you use in your future projects.
Description
In this section, we will write unit tests involving Actions. When testing Actions, we don’t typically test Actions directly. Instead, we test their use in Components, Effects, and Reducers. Throughout this course we will cover all these situations. For this section, we will verify that Actions are dispatched when expected by checking their use in Components.
Update dashboard.component.spec.ts
We will walk through updating src/app/dashboard/dashboard.component.spec.ts
to run tests for your Actions.
Setting Up our TestBed
When unit testing in general, we should use stubs to isolate parts of our application. Luckily NgRx makes this process simple by providing a way to create a mock Store
:
MockStore -
MockStore
extends theStore
class and provides stubs for its methods.provideMockStore() - Generates a
MockStore
instance given a configuration.
src/app/dashboard/dashboard.component.spec.ts
// src/app/dashboard/dashboard.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import * as fromLogin from '../store/login/login.reducer';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
let store: MockStore<fromLogin.LoginPartialState>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DashboardComponent],
providers: [provideMockStore({})],
}).compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
store = TestBed.inject(MockStore);
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('logout()', () => {
it('should dispatch logout action', () => {
// TODO: Spy on dispatching action
component.logout();
// TODO: Verify that LoginActions.logout action was dispatched
});
});
describe('username$', () => {
it('should get username from login state', () => {
// TODO: Verify username comes from login state
});
});
describe('userId$', () => {
it('should get userId from login state', () => {
// TODO: Verify userId comes from login state
});
});
});
Verify LoginActions.logout
Action Dispatches Properly
In src/app/dashboard/dashboard.component.spec.ts
, there is a describe
block with a TODO
: "Spy on dispatching action". We will create a spy to track when our MockStore
dispatches an Action:
src/app/dashboard/dashboard.component.spec.ts
// src/app/dashboard/dashboard.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import * as fromLogin from '../store/login/login.reducer';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
let store: MockStore<fromLogin.LoginPartialState>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DashboardComponent],
providers: [provideMockStore({})],
}).compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
store = TestBed.inject(MockStore);
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('logout()', () => {
it('should dispatch logout action', () => {
const spy = spyOn(store, 'dispatch');
component.logout();
// TODO: Verify that LoginActions.logout action was dispatched
});
});
describe('username$', () => {
it('should get username from login state', () => {
// TODO: Verify username comes from login state
});
});
describe('userId$', () => {
it('should get userId from login state', () => {
// TODO: Verify userId comes from login state
});
});
});
Next we will verify that the expected Action was dispatched after logout()
is called:
src/app/dashboard/dashboard.component.spec.ts
// src/app/dashboard/dashboard.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import * as LoginActions from '../store/login/login.actions';
import * as fromLogin from '../store/login/login.reducer';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
let store: MockStore<fromLogin.LoginPartialState>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DashboardComponent],
providers: [provideMockStore({})],
}).compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
store = TestBed.inject(MockStore);
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('logout()', () => {
it('should dispatch logout action', () => {
const spy = spyOn(store, 'dispatch');
component.logout();
expect(spy).toHaveBeenCalledOnceWith(LoginActions.logout());
});
});
describe('username$', () => {
it('should get username from login state', () => {
// TODO: Verify username comes from login state
});
});
describe('userId$', () => {
it('should get userId from login state', () => {
// TODO: Verify userId comes from login state
});
});
});
Note that there are 2 more pending
TODO
s insrc/app/dashboard/dashboard.component.spec.ts
that will be resolved in upcoming sections. For now we’ll only be testing our Actions.
Update login.component.spec.ts
We will walk through updating src/app/login/login.component.spec.ts
to run tests for your Actions.
Setting Up our TestBed
src/app/login/login.component.spec.ts
// src/app/login/login.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { LoginComponent } from './login.component';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import * as fromLogin from '../store/login/login.reducer';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let store: MockStore<fromLogin.LoginPartialState>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LoginComponent],
providers: [provideMockStore({}), FormBuilder],
imports: [ReactiveFormsModule],
}).compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
store = TestBed.inject(MockStore);
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('submit()', () => {
it('should mark form as touched', () => {
expect(component['form'].touched).toBe(false);
component.submit();
expect(component['form'].touched).toBe(true);
});
describe('when form is valid', () => {
const mock = {
username: 'some-username',
password: 'some-password',
};
beforeEach(() => {
component['form'].setValue(mock);
});
it('should have a valid form', () => {
// Verify that form is truly valid for upcoming tests
expect(component['form'].valid).toBe(true);
});
it('should dispatch LoginActions.login', () => {
// TODO: Spy on dispatching action
component.submit();
// TODO: Verify that LoginActions.login action was dispatched
});
});
describe('when form is NOT valid', () => {
const mock = {
username: 'some-username',
password: '', // password is required
};
beforeEach(() => {
component['form'].setValue(mock);
});
it('should NOT have a valid form', () => {
// Verify that form is truly invalid for upcoming tests
expect(component['form'].valid).toBe(false);
});
it('should NOT dispatch LoginActions.login', () => {
// TODO: Spy on dispatching action
component.submit();
// TODO: Verify that no action was dispatched
});
});
});
});
Verify LoginActions.login
Action Dispatches Properly With Form Payload
Unlike the LoginActions.logout
Action, LoginActions.login
Action requires a payload when dispatched. In our test, we will pass the state of our form:
src/app/login/login.component.spec.ts
// src/app/login/login.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { LoginComponent } from './login.component';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import * as LoginActions from '../store/login/login.actions';
import * as fromLogin from '../store/login/login.reducer';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let store: MockStore<fromLogin.LoginPartialState>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LoginComponent],
providers: [provideMockStore({}), FormBuilder],
imports: [ReactiveFormsModule],
}).compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
store = TestBed.inject(MockStore);
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('submit()', () => {
it('should mark form as touched', () => {
expect(component['form'].touched).toBe(false);
component.submit();
expect(component['form'].touched).toBe(true);
});
describe('when form is valid', () => {
const mock = {
username: 'some-username',
password: 'some-password',
};
beforeEach(() => {
component['form'].setValue(mock);
});
it('should have a valid form', () => {
// Verify that form is truly valid for upcoming tests
expect(component['form'].valid).toBe(true);
});
it('should dispatch LoginActions.login', () => {
const spy = spyOn(store, 'dispatch');
component.submit();
expect(spy).toHaveBeenCalledOnceWith(LoginActions.login(mock));
});
});
describe('when form is NOT valid', () => {
const mock = {
username: 'some-username',
password: '', // password is required
};
beforeEach(() => {
component['form'].setValue(mock);
});
it('should NOT have a valid form', () => {
// Verify that form is truly invalid for upcoming tests
expect(component['form'].valid).toBe(false);
});
it('should NOT dispatch LoginActions.login', () => {
// TODO: Spy on dispatching action
component.submit();
// TODO: Verify that no action was dispatched
});
});
});
});
Verify LoginActions.login
Action Does NOT Dispatch When Necessary
src/app/login/login.component.spec.ts
// src/app/login/login.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { LoginComponent } from './login.component';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import * as LoginActions from '../store/login/login.actions';
import * as fromLogin from '../store/login/login.reducer';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let store: MockStore<fromLogin.LoginPartialState>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LoginComponent],
providers: [provideMockStore({}), FormBuilder],
imports: [ReactiveFormsModule],
}).compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
store = TestBed.inject(MockStore);
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('submit()', () => {
it('should mark form as touched', () => {
expect(component['form'].touched).toBe(false);
component.submit();
expect(component['form'].touched).toBe(true);
});
describe('when form is valid', () => {
const mock = {
username: 'some-username',
password: 'some-password',
};
beforeEach(() => {
component['form'].setValue(mock);
});
it('should have a valid form', () => {
// Verify that form is truly valid for upcoming tests
expect(component['form'].valid).toBe(true);
});
it('should dispatch LoginActions.login', () => {
const spy = spyOn(store, 'dispatch');
component.submit();
expect(spy).toHaveBeenCalledOnceWith(LoginActions.login(mock));
});
});
describe('when form is NOT valid', () => {
const mock = {
username: 'some-username',
password: '', // password is required
};
beforeEach(() => {
component['form'].setValue(mock);
});
it('should NOT have a valid form', () => {
// Verify that form is truly invalid for upcoming tests
expect(component['form'].valid).toBe(false);
});
it('should NOT dispatch LoginActions.login', () => {
const spy = spyOn(store, 'dispatch');
component.submit();
expect(spy).not.toHaveBeenCalled();
});
});
});
});
Final Result
At the end of this section, the following spec file(s) should be updated. After each spec file has been updated and all the tests have passed, this means that all the previous sections have been completed successfully:
src/app/dashboard/dashboard.component.spec.ts
// src/app/dashboard/dashboard.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import * as LoginActions from '../store/login/login.actions';
import * as fromLogin from '../store/login/login.reducer';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
let store: MockStore<fromLogin.LoginPartialState>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DashboardComponent],
providers: [provideMockStore({})],
}).compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
store = TestBed.inject(MockStore);
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('logout()', () => {
it('should dispatch logout action', () => {
const spy = spyOn(store, 'dispatch');
component.logout();
expect(spy).toHaveBeenCalledOnceWith(LoginActions.logout());
});
});
describe('username$', () => {
it('should get username from login state', () => {
// TODO: Verify username comes from login state
});
});
describe('userId$', () => {
it('should get userId from login state', () => {
// TODO: Verify userId comes from login state
});
});
});
src/app/login/login.component.spec.ts
// src/app/login/login.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { LoginComponent } from './login.component';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import * as LoginActions from '../store/login/login.actions';
import * as fromLogin from '../store/login/login.reducer';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let store: MockStore<fromLogin.LoginPartialState>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LoginComponent],
providers: [provideMockStore({}), FormBuilder],
imports: [ReactiveFormsModule],
}).compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
store = TestBed.inject(MockStore);
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('submit()', () => {
it('should mark form as touched', () => {
expect(component['form'].touched).toBe(false);
component.submit();
expect(component['form'].touched).toBe(true);
});
describe('when form is valid', () => {
const mock = {
username: 'some-username',
password: 'some-password',
};
beforeEach(() => {
component['form'].setValue(mock);
});
it('should have a valid form', () => {
// Verify that form is truly valid for upcoming tests
expect(component['form'].valid).toBe(true);
});
it('should dispatch LoginActions.login', () => {
const spy = spyOn(store, 'dispatch');
component.submit();
expect(spy).toHaveBeenCalledOnceWith(LoginActions.login(mock));
});
});
describe('when form is NOT valid', () => {
const mock = {
username: 'some-username',
password: '', // password is required
};
beforeEach(() => {
component['form'].setValue(mock);
});
it('should NOT have a valid form', () => {
// Verify that form is truly invalid for upcoming tests
expect(component['form'].valid).toBe(false);
});
it('should NOT dispatch LoginActions.login', () => {
const spy = spyOn(store, 'dispatch');
component.submit();
expect(spy).not.toHaveBeenCalled();
});
});
});
});
Wrap-up: By the end of this section, your code should match this branch. You can also compare the code changes for our solution to this section on GitHub or you can use the following command in your terminal:
git diff origin/test-actions