Application testing
Angular Jasmine TDD Typescript Visual Code

How to Use Test Doubles to Mock Angular Unit Tests

Welcome to today’s post.

In today’s post I will be giving an overview of unit testing an Angular component and explain the importance of using mocks and stubs for implementing unit tests within an Angular application.

The principles that apply for unit testing backend services, web services and API services apply in many cases equally to front-end applications.

Creating Mocks, Stubs, Fakes is a necessity in unit testing.

I will define each in turn:

Stubs

A stub is an object created as a dependency of the class being tested, with the object not dependent on any test cases within the test class. Essentially, a stub does nothing within the test class.

Mocks

A mock is an object created as a dependency of the class being tested, with the object directly dependent at least one test case within the test class. A mock could contain variables to help determine the state of the test class.

Fakes

Both Stubs and Mocks are Fakes. Their usage within the test class determines the type of object.

Suppose I have a books component:

@Component({
    selector: 'books',
    templateUrl: './books.component.html'
})
export class BooksComponent {

    constructor(private api: ApiService, private router: Router) {}

    ngOnInit() {
	..
    }
...

As you can see, to implement a test for the Books component, we will have a constructor that takes an ApiService and Router.

For an end to end integration test that runs each component and renders the output and interactions with external services and other application components, each constructor parameter will be injected as an object of the instantiated class  provider specified in the providers section of our app module:

  providers: [
    ApiService, 
    AuthService,
    {
        provide: HTTP_INTERCEPTORS,
        useClass: AuthInterceptor,
        multi: true
    },
    LookupService, 
    BookDetailComponentPageResolver
  ],

When we are writing unit tests for our components and we are only testing the component itself and no other dependencies or interactions, then we can replace the constructor parameters with stubs or mock components.

Remember our earlier definitions:

Stubs are instantiations of our dependency class that do nothing.

Mocks are minimal instantiations of our dependency class that do enough for us to pass tests. Both Mocks and Stubs are Test Doubles.

Out mock API is very minimal and does not utilize external services. It simply uses test data as shown:

import { Book } from '../../models/Book';

export function getBooks(): Book[] {
return [
    { id: 1, title: 'Lord of the Rings', author: '', genre: '', 
      isbn: '', yearPublished: 2020, edition: '', location: '', 
      dateCreated: new Date(), dateUpdated: new Date() },
    { id: 2, title: 'The Hobbit', author: '', genre: '', 
      isbn: '', yearPublished: 2020, edition: '', location: '', 
      dateCreated: new Date(), dateUpdated: new Date() },
    { id: 3, title: 'Of Mice and Men', author: '', genre: '', 
      isbn: '', yearPublished: 2020, edition: '', location: '', 
      dateCreated: new Date(), dateUpdated: new Date() }
  ];
}

When creating our test suite, to configure our test imports, declarations, and providers, we use the namespace:

TestBed.configureTestingModule

With our providers we can declare a Jasmine Spy object test double for our router as shown:

let mockRouter = {
    navigate: jasmine.createSpy('navigate')
};

A mock Test API Service is the definition below:

import { Injectable } from '@angular/core'
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
import { Book } from 'src/app/models/Book';
import { Observable, Subject, of } from 'rxjs'
import { ApiService } from '../services/api.service';
import { getBooks } from '../services/test/test-books';

@Injectable()
export class TestApiService extends ApiService {
    books = getBooks();

    constructor() {
        super(null);
    }

    getBooks(): Observable<Book[]> {
        return of(this.books);
    }

    getBook(id): Observable<Book>
    {
        return of(this.books.find(b => b.id === id));
    }
	…
}

With the Books component we will be testing it in isolation, so the router dependency will be a stub as we will not be using it to test another class attached to the Books component. The API service, which is used to control the state of the component will be a mock.

Our component unit test setup and implementation with mocking is shown:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { MaterialModule } from '../material/material.module';
import { BooksComponent } from './books.component';
import { TestApiService } from '../services/test-api.service';
import { ApiService } from '../services/api.service';
import { Router } from '@angular/router';
import { getBooks } from '../services/test/test-books';

const books = getBooks();

describe('BooksComponent', () => {
  let component: BooksComponent;
  let fixture: ComponentFixture<BooksComponent>;
  let mockRouter = {
    navigate: jasmine.createSpy('navigate')
  };

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ BooksComponent ],
      imports: [ MaterialModule ],
      providers: [
        { provide: ApiService, useClass: TestApiService }, 
        { provide: Router, useValue: mockRouter }
      ]      
    })
    .compileComponents();
  }));

The mock TestAPIService can be replaced with a Spy test double and will achieve the same test outcome:

const mockService = jasmine.createSpyObj('TestApiService', 
['getBooks', 'getBook'] );
mockService.getBooks.and.returnValue(of(books));
mockService.getBook.and.returnValue(of(books.find(b => b.id === 1)));  

beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ BooksComponent ],
      imports: [ MaterialModule ],
      providers: [
        { provide: ApiService, useValue: mockService }, 
        { provide: Router, useValue: mockRouter }
      ]      
 })

The createSpyObj() above creates a class mockService, with two methods getBooks() and getBook(). This is much quicker than implementing an equivalent mock class.

With classes that are more complex, the recommendation would be to use a manually constructed mock class. For simpler scenarios, the Spy equivalent test double is sufficient.

When testing components at a component level here are recommended guidelines:

  1. Where a dependent service’s methods are not being used in the test, then the dependent service can be instanced as a stub.
  2. Where a dependent service’s methods are being used in the test, then the dependent service can be instanced as a mocked class.
  3. It is not necessary to instance the real class as the dependent services are not needed in a unit test – they are only required in an integration test or end to end test.

That’s all for today’s post.

I hope you found this post useful.

Social media & sharing icons powered by UltimatelySocial