Table of Contents
Introduction
If your project was created using the Angular CLI, everything will be ready for you to start writing tests using Jasmine as the testing framework and Karma as the test runner.
Angular also provides utilities like TestBed and async to make testing asynchronous code, components, directives, or services easier.
In this article, you will learn about writing and running unit tests in Angular using Jasmine and Karma.
Prerequisites
To complete this tutorial, you will need:
- Node.js installed locally, which you can do by following How to Install Node.js and Create a Local Development Environment.
- Some familiarity with setting up an Angular project.
This tutorial was verified with Node v16.2.0, npm v7.15.1, and @angular/core v12.0.4.
Step 1 — Setting Up the Project
Your test files are usually placed right alongside the files that they test, but they can just as well be in their own separate directory if you prefer.
These spec files use the naming convention of *.spec.ts.
First, use @angular/cli to create a new project:
ng new <^>angular-unit-test-example<^>
Then, navigate to the newly created project directory:
cd <^>angular-unit-test-example<^>
Alongside the app.component, there will be a app.component.spec.ts file. Open this file and examine its contents:
[label src/app/app.component.spec.ts]
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'angular-unit-test-example'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('angular-unit-test-example');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('angular-unit-test-example app is running!');
});
});
Understanding Jasmine
First, a few things that are important to know about Jasmine:
describeblocks define a test suite and eachitblock is for an individual test.
beforeEachruns before each test and is used for thesetuppart of a test.
afterEachruns after each test and is used for theteardownpart of a test.
- You can also use
beforeAllandafterAll, and these run once before or after all tests.
- You test an assertion in Jasmine with
expectand using a matcher liketoBeDefined,toBeTruthy,toContain,toEqual,toThrow,toBeNull, … For example:expect(myValue).toBeGreaterThan(3);
- You can do negative assertion with
not:expect(myValue).not.toBeGreaterThan(3);
- You can also define custom matchers.
TestBed is the main utility available for Angular-specific testing. You'll use TestBed.configureTestingModule in your test suite's beforeEach block and give it an object with similar values as a regular NgModule for declarations, providers, and imports. You can then chain a call to compileComponents to tell Angular to compile the declared components.
You can create a component fixture with TestBed.createComponent. Fixtures have access to a debugElement, which will give you access to the internals of the component fixture.
Change detection isn't done automatically, so you'll call detectChanges on a fixture to tell Angular to run change detection.
Wrapping the callback function of a test or the first argument of beforeEach with async allows Angular to perform asynchronous compilation and wait until the content inside of the async block to be ready before continuing.
Understanding the Tests
This first test is named should create the app and it uses expect to check for the presence of the component with toBeTruthy().
The second test is named should have as title 'angular-unit-test-example' and it uses expect to check that the app.title value is equal to the string 'angular-unit-test-example' with toEqual().
The third test is named should render title and it uses expect to check the compiled code for the text 'angular-unit-test-example app is running!' with toContain().
In your terminal, run the following command:
ng test
All three tests will run and the test results will appear:
[secondary_label Output]
3 specs, 0 failures, randomized with seed 84683
AppComponent
* should have as title 'angular-unit-test-example'
* should create the app
* should render title
All three tests are currently passing.
Step 2 — Building an Example Component
Let's create a component that increments or decrements a value.
Open app.component.ts in your code editor and replace the following lines of code with the increment and decrement logic:
[label src/app/app.component.ts]
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
<^>value = 0;<^>
<^>message!: string;<^>
<^>increment() {<^>
<^>if (this.value < 15) {<^>
<^>this.value += 1;<^>
<^>this.message = '';<^>
<^>} else {<^>
<^>this.message = 'Maximum reached!';<^>
<^>}<^>
<^>}<^>
<^>decrement() {<^>
<^>if (this.value > 0) {<^>
<^>this.value -= 1;<^>
<^>this.message = '';<^>
<^>} else {<^>
<^>this.message = 'Minimum reached!';<^>
<^>}<^>
<^>}<^>
}
Open app.component.html in your code editor and replace the content with the following code:
[label src/app/app.component.html]
<h1>{{ value }}</h1>
<hr>
<button (click)="increment()" class="increment">Increment</button>
<button (click)="decrement()" class="decrement">Decrement</button>
<p class="message">
{{ message }}
</p>
At this point, you should have revised versions of app.component.ts and app.component.html.
Step 3 — Building the Test Suite
Revisit app.component.spec.ts with your code editor and replace it with these lines of code:
[label src/app/app.component.spec.ts]
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
let fixture: ComponentFixture<AppComponent>;
let debugElement: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
debugElement = fixture.debugElement;
}));
it('should increment and decrement value', () => {
fixture.componentInstance.increment();
expect(fixture.componentInstance.value).toEqual(1);
fixture.componentInstance.decrement();
expect(fixture.componentInstance.value).toEqual(0);
});
it('should increment value in template', () => {
debugElement
.query(By.css('button.increment'))
.triggerEventHandler('click', null);
fixture.detectChanges();
const value = debugElement.query(By.css('h1')).nativeElement.innerText;
expect(value).toEqual('1');
});
it('should stop at 0 and show minimum message', () => {
debugElement
.query(By.css('button.decrement'))
.triggerEventHandler('click', null);
fixture.detectChanges();
const message = debugElement.query(By.css('p.message')).nativeElement.innerText;
expect(fixture.componentInstance.value).toEqual(0);
expect(message).toContain('Minimum');
});
it('should stop at 15 and show maximum message', () => {
fixture.componentInstance.value = 15;
debugElement
.query(By.css('button.increment'))
.triggerEventHandler('click', null);
fixture.detectChanges();
const message = debugElement.query(By.css('p.message')).nativeElement.innerText;
expect(fixture.componentInstance.value).toEqual(15);
expect(message).toContain('Maximum');
});
});
We assign the fixture and debugElement directly in the beforeEach block because all of our tests need these. We also strongly type them by importing ComponentFixture from @angular/core/testing and DebugElement from @angular/core.
In our first test, we call methods on the component instance itself.
In the remaining tests, we use our DebugElement to trigger button clicks. Notice how the DebugElement has a query method that takes a predicate. Here we use the By utility and its css method to find a specific element in the template. DebugElement also has a nativeElement method, for direct access to the DOM.
We also used fixture.detectChanges in the last 3 tests to instruct Angular to run change detection before doing our assertions with Jasmine's expect.
Once you have made your changes, run the ng test command from the terminal:
ng test
This will start Karma in watch mode, so your tests will recompile every time a file changes.
[secondary_label Output]
4 specs, 0 failures, randomized with seed 27239
AppComponent
* should increment value in template
* should increment and decrement value
* should stop at 0 and show minimum message
* should stop at 15 and show maximum message
All four tests will be passing.
Conclusion
In this article, you will learn about writing and running unit tests in Angular using Jasmine and Karma. Now that you know about the main Angular testing utilities and can start writing tests for simple components.
Continue your learning with testing components with dependencies, testing services as well as using mocks, stubs, and spies.
You can also refer to the official documentation for an in-depth Angular testing guide.