Introduction to Unit Testing in Angular

It’s easy to get started with unit testing for Angular 2+ apps. If your projects was setup 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.

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.

Getting started

Your test files (called spec files; hence the .spec.ts) 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. Upon creating a new Angular 2+ project, the default spec file for a component will look like the following:

app.component.spec.ts

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

import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));

  it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));

  it(`should have as title 'app works!'`, async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('app works!');
  }));
});

The above simply tests that the app component is created and that the title property on the component class has the value app works!

This includes a lot of utilities already. Let’s simplify a little bit and just test the component class on its own so that we can then better understand the utilities that are available on top of that:

import { AppComponent } from './app.component';

describe('AppComponent', () => {
  let appComponent = new AppComponent();

  it('should create the app', () => {
    const app = appComponent;
    expect(app).toBeTruthy();
  });

  it('should have `app works!` title', () => {
    const title = appComponent.title;
    expect(title).toEqual('app works!');
  });
});

Angular Testing Utilities

TestBed & Component Fixtures

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.

Async

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.

A Simple Example

Let’s create a simple component that increments or decrements a value. Here are the internals of the component class:

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!';
  }
}

And here’s the template:

<h1>{{ value }}</h1>

<hr>

<button (click)="increment()" class="increment">Increment</button>
<button (click)="decrement()" class="decrement">Decrement</button>

<p class="message">
  {{ message }}
</p>

The test suite

Now here’s how the test suite for our component can look like:

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/decrement value', () => {
    fixture.componentInstance.increment();
    expect(fixture.componentInstance.value).toEqual(1);

    fixture.componentInstance.decrement();
    expect(fixture.componentInstance.value).toEqual(0);
  });

  it('should increment 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');
  });
});

A few things to notice:

  • 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 used fixture.detectChanges in the last 3 tests to instruct Angular to run change detection before doing our assertions with Jasmine’s expect.

Running Your Tests

Running your test suite is as easy as calling the ng test command from the terminal:

$ ng test

It’ll start Karma in watch mode, so your tests will recompile every time a file changes.


Now you know about the main Angular testing utilities and can start writing tests for simple components.

⛏ This only scratches the surface and more will come for topics like 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.

✖ Clear

🕵 Search Results

🔎 Searching...