Testing Redirect Effects page
Learn how to write unit tests for NgRx Effects that redirect the user.
Quick Start: You can checkout this branch to get your codebase ready to work on this section.
Overview
Verify navigation to the dashboard page occurs when
LoginActions.loginSuccess
Action dispatches.Verify navigation to the login page occurs when
LoginActions.logoutSuccess
Action dispatches.
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.
Description
When testing Effects, we will verify side-effects are executed properly when an appropriate Action is dispatched. In our case, we are working with Effects that cause navigation, so we can take advantage of the RouterTestingModule.
Update login.effects.spec.ts
We will walk through updating src/app/store/login/login.effects.spec.ts
to run tests for your Effects.
Updating our TestBed
When testing navigation in Angular, we can take advantage of the RouterTestingModule. Using the static withRoutes()
method, we can prepare our tests to navigate to a mock login and dashboard page:
src/app/store/login/login.effects.spec.ts
// src/app/store/login/login.effects.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable, of, throwError } from 'rxjs';
import { LoginEffects } from './login.effects';
import { Action } from '@ngrx/store';
import { provideMockStore } from '@ngrx/store/testing';
import * as LoginActions from './login.actions';
import { Credentials, LoginService } from 'ngx-learn-ngrx';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
@Component({
selector: 'app-mock',
})
class MockComponent {}
const mockLoginService = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
login: (credentials: Credentials) => {
return of({ userId: 'some-user-id', token: 'some-token' });
},
logout: () => of(null),
} as LoginService;
describe('LoginEffects', () => {
let actions$: Observable<Action>;
let effects: LoginEffects;
let loginService: LoginService;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{ path: '', component: MockComponent },
{ path: 'dashboard', component: MockComponent },
]),
],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
{
provide: LoginService,
useValue: mockLoginService,
},
],
});
effects = TestBed.inject(LoginEffects);
loginService = TestBed.inject(LoginService);
router = TestBed.inject(Router);
});
describe('login$', () => {
beforeEach(() => {
actions$ = of(
LoginActions.login({
username: 'some-username',
password: 'some-password',
})
);
});
it('should get dispatch LoginActions.loginSuccess on api success', done => {
const spy = spyOn(loginService, 'login').and.callThrough();
effects.login$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith({
username: 'some-username',
password: 'some-password',
});
expect(action).toEqual(
LoginActions.loginSuccess({
username: 'some-username',
userId: 'some-user-id',
token: 'some-token',
})
);
done();
});
});
it('should get dispatch LoginActions.loginFailure on api error', done => {
const spy = spyOn(loginService, 'login').and.returnValue(
throwError(() => new Error('some error message'))
);
effects.login$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith({
username: 'some-username',
password: 'some-password',
});
expect(action).toEqual(
LoginActions.loginFailure({
errorMsg: 'some error message',
})
);
done();
});
});
});
describe('logout$', () => {
beforeEach(() => {
actions$ = of(LoginActions.logout());
});
it('should get dispatch LoginActions.logoutSuccess on api success', done => {
effects.logout$.subscribe(action => {
expect(action).toEqual(LoginActions.logoutSuccess());
done();
});
});
it('should get dispatch LoginActions.logoutFailure on api error', done => {
const spy = spyOn(loginService, 'logout').and.returnValue(
throwError(() => new Error('some error message'))
);
effects.logout$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith();
expect(action).toEqual(
LoginActions.logoutFailure({
errorMsg: 'some error message',
})
);
done();
});
});
});
});
Verifying Navigation to Dashboard Page When LoginActions.loginSuccess
Action Dispatches
Here we can use a spy to verify Router
is used to navigate to the dashboard page:
src/app/store/login/login.effects.spec.ts
// src/app/store/login/login.effects.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable, of, throwError } from 'rxjs';
import { LoginEffects } from './login.effects';
import { Action } from '@ngrx/store';
import { provideMockStore } from '@ngrx/store/testing';
import * as LoginActions from './login.actions';
import { Credentials, LoginService } from 'ngx-learn-ngrx';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
@Component({
selector: 'app-mock',
})
class MockComponent {}
const mockLoginService = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
login: (credentials: Credentials) => {
return of({ userId: 'some-user-id', token: 'some-token' });
},
logout: () => of(null),
} as LoginService;
describe('LoginEffects', () => {
let actions$: Observable<Action>;
let effects: LoginEffects;
let loginService: LoginService;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{ path: '', component: MockComponent },
{ path: 'dashboard', component: MockComponent },
]),
],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
{
provide: LoginService,
useValue: mockLoginService,
},
],
});
effects = TestBed.inject(LoginEffects);
loginService = TestBed.inject(LoginService);
router = TestBed.inject(Router);
});
describe('login$', () => {
beforeEach(() => {
actions$ = of(
LoginActions.login({
username: 'some-username',
password: 'some-password',
})
);
});
it('should get dispatch LoginActions.loginSuccess on api success', done => {
const spy = spyOn(loginService, 'login').and.callThrough();
effects.login$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith({
username: 'some-username',
password: 'some-password',
});
expect(action).toEqual(
LoginActions.loginSuccess({
username: 'some-username',
userId: 'some-user-id',
token: 'some-token',
})
);
done();
});
});
it('should get dispatch LoginActions.loginFailure on api error', done => {
const spy = spyOn(loginService, 'login').and.returnValue(
throwError(() => new Error('some error message'))
);
effects.login$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith({
username: 'some-username',
password: 'some-password',
});
expect(action).toEqual(
LoginActions.loginFailure({
errorMsg: 'some error message',
})
);
done();
});
});
});
describe('loginSuccess$', () => {
beforeEach(() => {
actions$ = of(
LoginActions.loginSuccess({
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
})
);
});
it('should navigate to dashboard', done => {
const spy = spyOn(router, 'navigate').and.callThrough();
effects.loginSuccess$.subscribe(() => {
expect(spy).toHaveBeenCalledOnceWith(['dashboard']);
done();
});
});
});
describe('logout$', () => {
beforeEach(() => {
actions$ = of(LoginActions.logout());
});
it('should get dispatch LoginActions.logoutSuccess on api success', done => {
effects.logout$.subscribe(action => {
expect(action).toEqual(LoginActions.logoutSuccess());
done();
});
});
it('should get dispatch LoginActions.logoutFailure on api error', done => {
const spy = spyOn(loginService, 'logout').and.returnValue(
throwError(() => new Error('some error message'))
);
effects.logout$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith();
expect(action).toEqual(
LoginActions.logoutFailure({
errorMsg: 'some error message',
})
);
done();
});
});
});
});
Verifying Navigation to Login Page When LoginActions.logoutSuccess
Action Dispatches
Here we can use a spy to verify Router
is used to navigate to the login page:
src/app/store/login/login.effects.spec.ts
// src/app/store/login/login.effects.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable, of, throwError } from 'rxjs';
import { LoginEffects } from './login.effects';
import { Action } from '@ngrx/store';
import { provideMockStore } from '@ngrx/store/testing';
import * as LoginActions from './login.actions';
import { Credentials, LoginService } from 'ngx-learn-ngrx';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
@Component({
selector: 'app-mock',
})
class MockComponent {}
const mockLoginService = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
login: (credentials: Credentials) => {
return of({ userId: 'some-user-id', token: 'some-token' });
},
logout: () => of(null),
} as LoginService;
describe('LoginEffects', () => {
let actions$: Observable<Action>;
let effects: LoginEffects;
let loginService: LoginService;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{ path: '', component: MockComponent },
{ path: 'dashboard', component: MockComponent },
]),
],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
{
provide: LoginService,
useValue: mockLoginService,
},
],
});
effects = TestBed.inject(LoginEffects);
loginService = TestBed.inject(LoginService);
router = TestBed.inject(Router);
});
describe('login$', () => {
beforeEach(() => {
actions$ = of(
LoginActions.login({
username: 'some-username',
password: 'some-password',
})
);
});
it('should get dispatch LoginActions.loginSuccess on api success', done => {
const spy = spyOn(loginService, 'login').and.callThrough();
effects.login$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith({
username: 'some-username',
password: 'some-password',
});
expect(action).toEqual(
LoginActions.loginSuccess({
username: 'some-username',
userId: 'some-user-id',
token: 'some-token',
})
);
done();
});
});
it('should get dispatch LoginActions.loginFailure on api error', done => {
const spy = spyOn(loginService, 'login').and.returnValue(
throwError(() => new Error('some error message'))
);
effects.login$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith({
username: 'some-username',
password: 'some-password',
});
expect(action).toEqual(
LoginActions.loginFailure({
errorMsg: 'some error message',
})
);
done();
});
});
});
describe('loginSuccess$', () => {
beforeEach(() => {
actions$ = of(
LoginActions.loginSuccess({
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
})
);
});
it('should navigate to dashboard', done => {
const spy = spyOn(router, 'navigate').and.callThrough();
effects.loginSuccess$.subscribe(() => {
expect(spy).toHaveBeenCalledOnceWith(['dashboard']);
done();
});
});
});
describe('logout$', () => {
beforeEach(() => {
actions$ = of(LoginActions.logout());
});
it('should get dispatch LoginActions.logoutSuccess on api success', done => {
effects.logout$.subscribe(action => {
expect(action).toEqual(LoginActions.logoutSuccess());
done();
});
});
it('should get dispatch LoginActions.logoutFailure on api error', done => {
const spy = spyOn(loginService, 'logout').and.returnValue(
throwError(() => new Error('some error message'))
);
effects.logout$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith();
expect(action).toEqual(
LoginActions.logoutFailure({
errorMsg: 'some error message',
})
);
done();
});
});
});
describe('logoutSuccess$', () => {
beforeEach(() => {
actions$ = of(LoginActions.logoutSuccess());
});
it('should navigate to login', done => {
const spy = spyOn(router, 'navigate').and.callThrough();
effects.logoutSuccess$.subscribe(() => {
expect(spy).toHaveBeenCalledOnceWith(['']);
done();
});
});
});
});
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/store/login/login.effects.spec.ts
// src/app/store/login/login.effects.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable, of, throwError } from 'rxjs';
import { LoginEffects } from './login.effects';
import { Action } from '@ngrx/store';
import { provideMockStore } from '@ngrx/store/testing';
import * as LoginActions from './login.actions';
import { Credentials, LoginService } from 'ngx-learn-ngrx';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
@Component({
selector: 'app-mock',
})
class MockComponent {}
const mockLoginService = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
login: (credentials: Credentials) => {
return of({ userId: 'some-user-id', token: 'some-token' });
},
logout: () => of(null),
} as LoginService;
describe('LoginEffects', () => {
let actions$: Observable<Action>;
let effects: LoginEffects;
let loginService: LoginService;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{ path: '', component: MockComponent },
{ path: 'dashboard', component: MockComponent },
]),
],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
{
provide: LoginService,
useValue: mockLoginService,
},
],
});
effects = TestBed.inject(LoginEffects);
loginService = TestBed.inject(LoginService);
router = TestBed.inject(Router);
});
describe('login$', () => {
beforeEach(() => {
actions$ = of(
LoginActions.login({
username: 'some-username',
password: 'some-password',
})
);
});
it('should get dispatch LoginActions.loginSuccess on api success', done => {
const spy = spyOn(loginService, 'login').and.callThrough();
effects.login$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith({
username: 'some-username',
password: 'some-password',
});
expect(action).toEqual(
LoginActions.loginSuccess({
username: 'some-username',
userId: 'some-user-id',
token: 'some-token',
})
);
done();
});
});
it('should get dispatch LoginActions.loginFailure on api error', done => {
const spy = spyOn(loginService, 'login').and.returnValue(
throwError(() => new Error('some error message'))
);
effects.login$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith({
username: 'some-username',
password: 'some-password',
});
expect(action).toEqual(
LoginActions.loginFailure({
errorMsg: 'some error message',
})
);
done();
});
});
});
describe('loginSuccess$', () => {
beforeEach(() => {
actions$ = of(
LoginActions.loginSuccess({
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
})
);
});
it('should navigate to dashboard', done => {
const spy = spyOn(router, 'navigate').and.callThrough();
effects.loginSuccess$.subscribe(() => {
expect(spy).toHaveBeenCalledOnceWith(['dashboard']);
done();
});
});
});
describe('logout$', () => {
beforeEach(() => {
actions$ = of(LoginActions.logout());
});
it('should get dispatch LoginActions.logoutSuccess on api success', done => {
effects.logout$.subscribe(action => {
expect(action).toEqual(LoginActions.logoutSuccess());
done();
});
});
it('should get dispatch LoginActions.logoutFailure on api error', done => {
const spy = spyOn(loginService, 'logout').and.returnValue(
throwError(() => new Error('some error message'))
);
effects.logout$.subscribe(action => {
expect(spy).toHaveBeenCalledOnceWith();
expect(action).toEqual(
LoginActions.logoutFailure({
errorMsg: 'some error message',
})
);
done();
});
});
});
describe('logoutSuccess$', () => {
beforeEach(() => {
actions$ = of(LoginActions.logoutSuccess());
});
it('should navigate to login', done => {
const spy = spyOn(router, 'navigate').and.callThrough();
effects.logoutSuccess$.subscribe(() => {
expect(spy).toHaveBeenCalledOnceWith(['']);
done();
});
});
});
});
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-redirect-effects