Testing API Effects page
Learn how to write unit tests for NgRx Effects.
Quick Start: You can checkout this branch to get your codebase ready to work on this section.
Overview
Verify
LoginActions.loginSuccess
Action dispatches when API request is successful.Verify
LoginActions.loginFailure
Action dispatches when API request fails.Verify
LoginActions.logoutSuccess
Action dispatches when API request is successful.Verify
LoginActions.logoutFailure
Action dispatches when API request fails.
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 that a specific Action gets dispatched depending on the circumstances when an Effect is triggered. In our case, we are working with Effects that depend on an API response. We can take advantage of spies to simulate different API responses.
Update login.effects.spec.ts
We will walk through updating src/app/store/login/login.effects.spec.ts
to run tests for your Effects.
Setting Up our TestBed
When testing Effects, we will need to mock the Actions
class since it plays a major role on how Effects work. NgRx provides a convenient way to do this through provideMockActions()
:
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 } from 'rxjs';
import { LoginEffects } from './login.effects';
import { Action } from '@ngrx/store';
import { provideMockStore } from '@ngrx/store/testing';
import * as LoginActions from './login.actions';
describe('LoginEffects', () => {
let actions$: Observable<Action>;
let effects: LoginEffects;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
],
});
effects = TestBed.inject(LoginEffects);
});
describe('login$', () => {
beforeEach(() => {
actions$ = of(
LoginActions.login({
username: 'some-username',
password: 'some-password',
})
);
});
});
});
One way to mock Actions
is to set actions$
to be an Observable
that emits whatever Action we want for our tests:
// Note: This example code is not part of our application repo or solution
describe('login$', () => {
beforeEach(() => {
// Mock `actions$` with `Observable` that emits an Action
// and its payload for upcoming tests
actions$ = of(
LoginActions.login({
username: 'some-username',
password: 'some-password',
})
);
});
it('should...', () => {
effects.login$.subscribe(action => {
// This `action` value will be whatever Action is dispatched
// based on our mocked value for `actions$`
});
});
});
Mocking LoginService
Since our Effects use LoginService
from ngx-learn-ngrx
, we will need to also mock this Service
:
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 } 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';
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;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
{
provide: LoginService,
useValue: mockLoginService,
},
],
});
effects = TestBed.inject(LoginEffects);
loginService = TestBed.inject(LoginService);
});
describe('login$', () => {
beforeEach(() => {
actions$ = of(
LoginActions.login({
username: 'some-username',
password: 'some-password',
})
);
});
});
});
Verifying LoginEffects.login$
Effect Dispatches LoginActions.loginSuccess
When API Request is Successful
We will create a spy to verify LoginService.login()
is called with the expected arguments. Then we will verify that LoginActions.loginSuccess
is dispatched with the proper payload:
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 } 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';
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;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
{
provide: LoginService,
useValue: mockLoginService,
},
],
});
effects = TestBed.inject(LoginEffects);
loginService = TestBed.inject(LoginService);
});
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();
});
});
});
});
Note that we are taking advantage of the
done()
callback to write an asynchronous test. This is common when writing tests where you subscribe to anObservable
to perform a test.
Verifying LoginEffects.login$
Effect Dispatches LoginActions.loginFailure
When API Request is NOT Successful
To verify different behaviors for LoginService
, we can take advantage of spies to return a different return value:
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';
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;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
{
provide: LoginService,
useValue: mockLoginService,
},
],
});
effects = TestBed.inject(LoginEffects);
loginService = TestBed.inject(LoginService);
});
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();
});
});
});
});
Verifying LoginEffects.logout$
Effect Dispatches LoginActions.logoutSuccess
When API Request is Successful
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';
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;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
{
provide: LoginService,
useValue: mockLoginService,
},
],
});
effects = TestBed.inject(LoginEffects);
loginService = TestBed.inject(LoginService);
});
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();
});
});
});
});
Verifying LoginEffects.logout$
Effect Dispatches LoginActions.logoutFailure
When API Request is NOT Successful
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';
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;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
{
provide: LoginService,
useValue: mockLoginService,
},
],
});
effects = TestBed.inject(LoginEffects);
loginService = TestBed.inject(LoginService);
});
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();
});
});
});
});
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';
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;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
LoginEffects,
provideMockActions(() => actions$),
provideMockStore(),
{
provide: LoginService,
useValue: mockLoginService,
},
],
});
effects = TestBed.inject(LoginEffects);
loginService = TestBed.inject(LoginService);
});
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();
});
});
});
});
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-api-effects