Needless to say, software can’t be shipped to customers without testing it thoroughly. There are multiple ways to test an application - manual tests, automated integration tests, automated unit tests, performance tests and a few other techniques. Each of these techniques test different aspects of the application.
Of these, unit tests are always written by Developers.
This tutorial is from the DotNetCurry(DNC) Magazine with in-depth tutorials and best practices in JavaScript and .NET. This magazine is aimed at Developers, Architects and Technical Managers and covers Angular, React, Vue.js, C#, Design Patterns, .NET Core, MVC, Azure, DevOps, ALM, and more. Subscribe to this magazine for FREE and receive all previous, current and upcoming editions, right in your Inbox. No Spam Policy.
Unit test is the technique that tests a piece of code in isolation. The piece of code has to be independent of any possible side effects when a set of unit tests are run on it.
Unit tests play an important role in software development. They help in catching the bugs early and ensure quality of the code. Unit tests also provide documentation for the code. If a file has enough number of unit tests and each test is described well, a new developer in the team would be able to understand code in that file, without any extra effort.
Not every piece of code written is unit testable.
The code has to follow a set of conventions so that it can be isolated while testing. Testability of the code depends on the code itself and the platform on which the code is written.
Angular is built with testing in mind. The modular architecture and the way Dependency Injection works, makes any of the Angular code blocks easier to test.
This tutorial will show how Angular CLI sets up the environment for unit testing and then shows how to test Angular components.
Testing Setup in Angular CLI
Create a new Angular application using Angular CLI. Run the following command to get the application created:
> ng new visitingplaces
The project created using Angular CLI contains the required setup to write and run the tests. The following set of packages are installed to support testing using Karma and Jasmine:
Figure 1 – Packages for Karma and Jasmine installed by Angular CLI
The test setup is added to the file karma.conf.js inside the src folder. The following snippet shows the default code in the karma.conf.js file:
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};
The default framework used in this project is Jasmine.
The package @angular-devkit/build-angular contains the logic of building the webpack configuration for tests. It reads the angular.json configuration file and prepares the objects for running karma.
Plugins are the set of extensions we need to run the tests. Every framework configured in karma should also have a plugin added, so the list includes a plugin for Jasmine and one for @angular-devkit.
Karma also needs providers for the browser to be used, to generate test reports and to generate the code coverage. These plugins are configured using their respective NPM packages.
The coverageIstanbulReporter option contains the path where the coverage report has to be stored, the type of reports to be generated.
Rest of the configuration options set the reports to be generated after running the tests, port number on which Karma has to run, to keep karma watching for changes to the files, the browser to be used to run the tests and the singleRun option tells whether to exit after running the tests once.
The generated project contains some default tests. Let’s run them using the following command:
> ng test
This command generates output similar to the following:
Figure 2 – Test result on the console
As the tests run on Chrome, a new instance of Chrome pops up with the URL set to http://localhost:9876 while running these tests. The page on the browser shows the list of tests executed and their status.
The following screenshot shows the output on the browser:
Figure 3 – Test result on the browser
Though the tests are executed in Chrome, it is preferred to use a headless browser like Chrome Headless to run unit tests in the CI/CD processes. For this, the package puppeteer has to be installed and ChromeHeadless has to be created as a custom launcher in the karma configuration. The following command installs the package puppeteer:
> npm install puppeteer --save -dev
This package has to be imported in the file karma.conf.js and the path has to be set to the environment. The following statements achieve this task:
const puppeteer = require('puppeteer');
process.env.CHROME_BIN = puppeteer.executablePath();
The following snippet configures the custom launcher for karma:
customLaunchers: {
ChromeHeadless: {
base: 'Chrome',
flags: [
'--headless',
'--remote-debugging-port=9222',
'--no-sandbox',
'--proxy-server=\'direct://\'',
'--proxy-bypass-list=*'
],
}
},
Change the browser options to use ChromeHeadless.
browsers: ['ChromeHeadless'],
As the configuration shows, the custom launcher opens chrome in the headless mode. The browser won’t be visible on the screen, but the Chromium engine would be used in the background to run the tests.
On running the tests now, you will find similar result on the console.
The only difference is, you won’t see a new instance of the Chrome browser now. Hence, debugging would be difficult. This is OK as we won’t use a CI/CD server for debugging.
Types of Tests
An Angular component can be tested in a number of different ways. This article will use the following techniques:
- Component Class testing: testing the component class alone in isolation
- Shallow Integration Testing: Testing the component using the APIs provided for tests after shallowing any other components or directives used
- Deep Integration Testing: Testing a component by creating stubs for the components and directives surrounding it
Learning From the Generated Tests
The code generated by the Angular CLI contains tests for the AppComponent. Reading the tests in this file would provide a good starting point.
Open this file in Visual Studio Code. The following snippet shows the initial statements in the file:
import { TestBed, async } from '@angular/core/testing'; // 1
import { AppComponent } from './app.component'; // 2
describe('AppComponent', () => {
beforeEach(async(() => { // 3
TestBed.configureTestingModule({ // 4
declarations: [
AppComponent
],
}).compileComponents(); // 5
}));
//
});
The following listing explains the statements marked with numbers in the above snippet:
1. Statement 1 imports the TestBed and async objects from the package @angular/core/testing. The TestBed object is used to initialize the testing environment for an Angular object. The async method is used to wrap a piece of code in a test zone. It detects any asynchronous calls made in a block of code and completes the test when all the asynchronous calls are complete.
2. Statement 2 imports the component to be tested
3. Statement 3 is the beginning of the beforeEach block, this block is used to set up the environment and the objects required for the tests in this file. It runs before every test and for any sub-level describe blocks. The beforeEach block ensures that the state modified in a test doesn’t cause any side effects in the next test
4. Statement 4 configures a test module using the component to be tested. It declares the component to be tested in the same way as the components are declared in the module
5. Statement 5 calls the compileComponents method on the test module. This is done to load the templates referred in the component using the templateUrl property and then to compile the components so that they can be created in the tests
Now let’s understand the tests in this file. The following snippet shows the first test:
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
This test simply checks if the component instance is created. It creates the fixture of the component using the TestBed.createComponent method. The debugElement property of the fixture object provides all the required debugging APIs of the component. The componentInstance property of the debugElement is used to get object of the component and then the assertion checks whether this object is created.
The next two tests check if the component’s DOM is rendered correctly. Let’s understand the third test. It is shown below:
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to visitingplaces!');
}));
This test checks if the h1 element in the template is correctly rendered.
The h1 element contains a binding expression that binds the title property of the component class. Angular updates the bindings in the template when the change detection runs. The fixture.detectChanges method runs the change detection on the component and updates the bindings.
Now we need the root DOM object of the component. This object will be used to query the elements containing bindings and then to check if they contain the right data.
Fixture.debugElement.nativeElement gives the DOM object of the component. All the standard DOM APIs can be applied on this object. The h1 element is selected using querySelector and then the test checks if it has the right content set.
The tests generated for the AppComponent are integration tests as these tests are based on the component instance. The test is not using shallows as the template of AppComponent doesn’t contain any other directives or components. We will discuss the other techniques in the next section.
Testing More Scenarios
The tests generated in Angular CLI provided some foundation to write tests for the components. Now let’s write tests for a few components that cover more real-life scenarios.
If you want to follow along with rest of the article, use code in the folder named before from the sample code. This folder contains a simple Angular application with two components: PlacesComponent and PlaceComponent.
The following screenshot shows how this application looks:
Figure 4 – Output of the sample application
The PlacesComponent contains a select box with a list of places. It passes the selected place to the PlaceComponent as an input parameter. The PlaceComponent displays details of the place it accepts and raises an event when the button in the component is clicked.
In this section, we will write tests for both of these components.
Testing PlaceComponent
The PlaceComponent accepts a place object and displays the details of this place on the screen. It emits an event when the Button is clicked.
Let’s test the functionality of this component in the three ways discussed earlier. To write these tests, add a file named place.compone.spec.ts to the app folder.
Testing as a Class
Testing a component as a class is similar to testing any TypeScript class. To test it, one needs to create an object of the class and invoke the functionality. Open the file place.component.spec.ts and add the following code to it:
import { PlaceComponent } from './place.component';
describe('PlaceComponent as class', () => {
let placeComponent: PlaceComponent;
beforeEach(() => {
placeComponent = new PlaceComponent();
placeComponent.place = {
name: 'Charminar',
city: 'Hyd',
country: 'India',
isVisited: true,
rating: 4
};
});
If you are following along and writing the tests, have the ng test command running on a command prompt to see the test results as you make the changes.
As you see, the beforeEach block creates an instance of the PlaceComponent class and sets value to the property place. The PlaceComponent class has the property IsVisited. It returns Yes if the place is visited and No if the place is not visited.
The following tests verify this behavior:
it('should return Yes when the place is visited', () => {
expect(placeComponent.IsVisited).toEqual('Yes');
});
it('should return No when the place is not visited', () => {
placeComponent.place.isVisited = false;
expect(placeComponent.IsVisited).toEqual('No');
});
The method togglePlaceVisited emits an event using the field toggleVisited. As this field is an EventEmitter, we can test if the emitted event contains the right data by subscribing to it. The following test verifies this:
it('should emit event when toggleVisited is called', () => {
placeComponent.toggleVisited.subscribe(name => expect(name).toEqual('Charminar'));
placeComponent.togglePlaceVisited();
});
Testing as an Independent Component
To test as a component, we need to load a few objects from the libraries that were installed along with Angular. The following import statements load them:
import { NO_ERRORS_SCHEMA, Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Place } from './place.model';
As you would have already guessed, we need to create a fixture object for the PlaceComponent in the beforeEach block. The following snippet creates the fixture:
describe('PlaceComponent Tests: As an independent component', () => {
let place: Place,
rootElement: HTMLElement;
let fixture: ComponentFixture<PlaceComponent>;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [PlaceComponent]
})
.createComponent(PlaceComponent);
place = {
name: 'Charminar',
city: 'Hyd',
country: 'India',
isVisited: true,
rating: 4
};
rootElement = fixture.debugElement.query(By.css('.thumbnail')).nativeElement as HTMLElement;
});
});
The above beforeEach block creates the component fixture using a test module and then gets the root element of the component. To get it, it queries the debugElement using the By.css predicate.
The first test will check if the component instance is created. It is shown below:
it('should have initialized the component', () => {
expect(fixture.componentInstance).toBeDefined();
});
A more useful test would be to check the behavior of the component after changing the data it accepts.
The isVisited property on the place is set to true, so we have the class visited-place set to the root element and the toggle button has the text Mark Not Visited. This behavior can be tested by inspecting the elements in the component. The following snippet tests this behavior:
it('should have applied the changes when the place set is visited', () => {
fixture.componentInstance.place = place;
fixture.detectChanges();
expect(rootElement.classList).toContain('visited-place');
expect(rootElement.querySelector("div.caption").textContent)
.toEqual(place.name);
expect(rootElement.querySelector('a').textContent).toEqual('Mark Not Visited');
});
Notice the call to detectChanges after setting the value of place. Bindings in the component are checked again and the view gets updated if state of the objects has changed.
When the place is not visited, the root element won’t have the visited-place class set and the button will say Mark Visited. The following test verifies this:
it('should have applied the changes when the place set is not visited', () => {
place.isVisited = false;
fixture.componentInstance.place = place;
fixture.detectChanges();
expect(rootElement.classList).not.toContain('visited-place');
expect(rootElement.querySelector('a').textContent).toEqual('Mark Visited');
});
When the button is clicked, the component raises the toggleVisited event. This can be tested by triggering event on the button using the DOM API.
The following snippet shows this test:
it("should emit the event when the button is clicked", () => {
fixture.componentInstance.place = place;
fixture.detectChanges();
fixture.componentInstance.toggleVisited.subscribe((name) => expect(name).toEqual(place.name));
rootElement.querySelector('a').click();
});
Testing using a Host Component
In an application using the PlaceComponent, the input property would be sent from the host component and the output event is also consumed by the host component.
In the sample application, the PlacesComponent hosts the PlaceComponent. It would be good to test this functionality using a mock host component, as that would really test if the PlaceComponent interacts correctly with the host.
The setup of this test suit will be different, as it will create a mock component. The mock component would be declared in the test module along with PlaceComponent. And the test will create a fixture object using the mock component instead of the component to be tested.
The following snippet sets up the required objects for the test suit:
describe('PlaceComponent Tests: Inside a test host', () => {
@Component({
template: `<place [selectedPlace]="selectedPlace" (toggleVisited)="toggleVisited($event)"></place>`
})
class TestHostComponent {
places: Place[] = [{
name: 'Charminar',
city: 'Hyderabad',
country: 'India',
isVisited: true,
rating: 4
},
{
name: 'Taj Mahal',
city: 'Agra',
country: 'India',
isVisited: false,
rating: 3
}];
selectedPlace: Place;
placeName: string;
constructor() {
this.selectedPlace = this.places[0];
}
toggleVisited(name: string) {
this.placeName = name;
}
}
let rootElement: HTMLElement;
let fixture: ComponentFixture<TestHostComponent>;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [PlaceComponent, TestHostComponent]
})
.createComponent(TestHostComponent);
rootElement = fixture.debugElement.query(By.css('.thumbnail')).nativeElement as HTMLElement;
});
});
As you can see, the component TestComponentHost uses the PlaceComponent in its template. It binds a value to the selectedPlace input property and binds a method with the output property toggleVisited.
The fixture object is created using TestHostComponent.
The tests will manipulate data using the fixture object and state of the DOM would be asserted using root element of the PlaceComponent.
The first test will check for the initial state of the PlaceComponent. The TestHostComponent assigns a value to the field selectedPlace in the constructor, so the test will check if the DOM has the values in the object assigned. The following snippet shows this test:
it('should have the place component', () => {
fixture.detectChanges();
expect(rootElement.querySelector('.caption').textContent).toEqual('Charminar');
});
When the event toggleVisited is triggered, the TestHostComponent should receive name of the place. The following test asserts this case by clicking the button programmatically:
it('should emit name of the place', () => {
fixture.detectChanges();
let visitedLink = fixture.debugElement.query(By.css('a')).nativeElement as HTMLElement;
visitedLink.click();
expect(fixture.componentInstance.placeName).toEqual(fixture.componentInstance.selectedPlace.name);
});
When TestHostComponent changes the place, bindings in the PlaceComponent have to be updated. This can be tested by assigning a new place to selectedPlace. The following test does this:
it('should change bindings when place is updated', () => {
fixture.componentInstance.selectedPlace = fixture.componentInstance.places[1];
fixture.detectChanges();
expect(rootElement.querySelector('.caption').textContent).toEqual('Taj Mahal');
});
Testing PlacesComponent
The PlacesComponent uses the service PlacesService and the PlaceComponent in its template.
Both of these have to be mocked while testing the PlacesComponent. It also uses the built-in directive ngModel on the select element.
As it is a built-in directive, you may load the Forms Module to the test module and test the component with the behavior of ngModel.
For the example test, I am going to shallow this directive, which means I will ask Angular to ignore any unknown HTML elements and attributes. For this, we need to import the schema NO_ERRORS_SCHEMA from @angular/core module and add it to schemas of the module as shown here:
import { NO_ERRORS_SCHEMA } from '@angular/core';
//
beforeEach(() => {
//
fixture = TestBed.configureTestingModule({
declarations: [MyComponent],
schemas: [NO_ERRORS_SCHEMA]
})
.createComponent(MyComponent);
});
While testing a component that uses a service, the service has to be mocked. This is done to isolate the component from any side effects that the service could cause. The service is replaced with a mock implementation.
To create the mock effectively, use the provider name of the service and assign it with a different class, factory function or an object.
The following snippet shows the setup for testing PlacesComponent with the mocks of PlaceComponent and PlacesService:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NO_ERRORS_SCHEMA, Component, Output, Input, EventEmitter } from '@angular/core';
import { PlacesComponent } from './places.component';
import { PlacesService } from './places.service';
import { Place } from './place.model';
@Component({
selector: 'place',
template: '<div></div>'
})
class MockPlaceComponent {
@Input('selectedPlace')
place: Place;
@Output('toggleVisited')
toggleVisited = new EventEmitter<string>();
}
describe('PlacesComponent tests', () => {
let fixture: ComponentFixture<PlacesComponent>,
service: PlacesService;
let mockService;
beforeEach(() => {
mockService = {
places: [{
name: 'Charminar',
city: 'Hyd',
country: 'India',
isVisited: true,
rating: 4
},
{
name: 'Taj Mahal',
city: 'Agra',
country: 'India',
isVisited: false,
rating: 3
}],
toggleVisited: jasmine.createSpy('toggleVisited')
};
fixture = TestBed.configureTestingModule({
declarations: [PlacesComponent, MockPlaceComponent],
providers: [{ provide: PlacesService, useValue: mockService }],
schemas: [NO_ERRORS_SCHEMA]
})
.createComponent(PlacesComponent);
service = fixture.componentRef.injector.get(PlacesService); //getting instance of the service
fixture.detectChanges();
});
});
The service is mocked here using the useValue provider. The mock object created has the public members of the PlaceService class. It assigns a static array to the places property and a Jasmine spy method to the toggleVisited method.
Observe the statement that’s used to get instance of the service. Injector of the component is used to get the service instance and the injection token is passed to the injector.get method.
Once the component is initialized, the select box should be populated with data in the places array. The following test verifies this:
it('should have select box populated with all places', () => {
expect(fixture.debugElement.queryAll(By.css('option')).length).toEqual(2);
});
By default, the PlacesComponent selects first item in the places array and it is passed to the place component. The next test verifies if the place component has received the right place.
it('should have assigned the selected date to place component', () => {
let placeDebugElement = fixture.debugElement.query(By.css('place'));
let place = placeDebugElement.componentInstance.place;
expect(place.name).toEqual('Charminar');
});
Once selectedPlace is modified in the PlacesComponent, the new place has to be assigned to the PlaceComponent. The following test verifies this:
it('should have assigned the selected date to place component when the selection changes', () => {
fixture.componentInstance.selectedPlace = mockService.places[1];
fixture.detectChanges();
let placeDebugElement = fixture.debugElement.query(By.css('place'));
let place = placeDebugElement.componentInstance.place;
expect(place.name).toEqual('Taj Mahal');
});
When the toggleVisited method on the component is called, it should call the toggleVisited method on the service. The following test verifies this:
it('should call the toggle method in the service', () => {
fixture.componentInstance.toggleVisited('some name');
expect(mockService.toggleVisited).toHaveBeenCalledWith('some name');
});
Conclusion
Unit testing is an important aspect to test applications as it ensures quality of the code along with reducing the possible bugs.
As shown in this tutorial, Angular makes the process of testing easier by isolating each block of code from the other and by providing a number of helper APIs to work with the framework while testing.
Components handle the most important aspects of an application and they have a lot of variations. Hope this tutorial got you started with testing different types of components. The future articles will discuss unit testing of other code blocks.
Download the entire source code from GitHub at bit.ly/dncm38-unittesting-angular
This article was technically reviewed by Keerti Kotaru.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Rabi Kiran (a.k.a. Ravi Kiran) is a developer working on Microsoft Technologies at Hyderabad. These days, he is spending his time on JavaScript frameworks like AngularJS, latest updates to JavaScript in ES6 and ES7, Web Components, Node.js and also on several Microsoft technologies including ASP.NET 5, SignalR and C#. He is an active
blogger, an author at
SitePoint and at
DotNetCurry. He is rewarded with Microsoft MVP (Visual Studio and Dev Tools) and DZone MVB awards for his contribution to the community