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. The reason is that when unit testing, we would not need the full functionality of dependencies to test specific objects in isolation. Where we require some basic tests of business logic to be present, the use of mocks is sufficient, and stubs where no tests are required in the dependencies. To facilitate these tests, we can make use of mock data that can represent a dependency of the object(s) to be tested.
Before I provide examples of how to convert a class to use test doubles, I will define subs, mocks and fakes in the first section.
Defining Stubs, Mocks and Fakes
I will define each test object 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.
Replacing Component Dependencies with Mocks or Stubs
To get a sense for the above terminologies, I will go through an example.
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.
Implementation of a Mock Web API Service
The mock web API I will use as an example 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));
}
…
}
In the next section, I will show how to integrate test dependency stubs into the above class.
Using Dependency Stubs
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:
- Where a dependent service’s methods are not being used in the test, then the dependent service can be instanced as a stub.
- Where a dependent service’s methods are being used in the test, then the dependent service can be instanced as a mocked class.
- 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.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.