Tutorial

How To Get Started with Unit Tests for Angular

Angular

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:

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:

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:

  • describe blocks define a test suite and each it block is for an individual test.
  • beforeEach runs before each test and is used for the setup part of a test.
  • afterEach runs after each test and is used for the teardown part of a test.
  • You can also use beforeAll and afterAll, and these run once before or after all tests.
  • You test an assertion in Jasmine with expect and using a matcher like toBeDefined, 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:

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:

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:

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:

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.

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.

Creative Commons License