Intro to Unit Testing In Angular With Karma & Jasmine

Ah, unit testing. One of my favorite subjects. I’ve been big on unit tests for what seems like more than a decade now, but I’ve never dipped my toes into the UI unit test pool! It’s been long enough, though. It’s time to learn!

So, I’m taking my first look at writing tests for an Angular application using Karma and Jasmine. I thought this tutorial at scotch.io was a great first read. It provides overviews of what Karma and Jasmine are, and walks you through the steps of creating an Anuglar application and adding tests from scratch using the Angular CLI, and that’s basically the foundation I’m working with today.

In this article, I’m going to write some tests as I extend behavior on “an existing product” and also write tests for some untested code. I’ll be using my favorite boilerplate code–the .NET Core CLI Angular template–as my existing product, as it provides a functional appliction with some existing tests for us to build on.

If you haven’t already, start by creating a new Angular application by running the following command:

dotnet new angular -o my-app

Verify that can run your new application by running dotnet run in the my-app directory. my-app/ClientApp contains the Angular application. Since we’re going to be adding to existing test suite, we should verify that the existing tests run and pass. Run the following from my-app/ClientApp:

npm install
ng test

You should see output like the following, indicating that all tests have passed.

Now that we’ve verified that existing tests work, we can start making changes. Let’s assume we want to modify the Counter page to have a decrement button in addition to its increment button, because sometimes we click too many times. Before we start making changes to the Counter component itself, we can describe the desired behavior in the existing counter.component.spec.ts file, which already contains similar logic for the increment functionality. Most of what needs to be written can be copied and adjusted from the Increment test.

Here’s the test I wrote:

it ('should decrease the current count by 1 when Decrement is clicked', async(() => {
  const countElement = fixture.nativeElement.querySelector('strong');
  expect(countElement.textContent).toEqual('0');

  const decrementButton = fixture.nativeElement.querySelector('#decrement-button');
  decrementButton.click();
  fixture.detectChanges();
  expect(countElement.textContent).toEqual('-1');
}));

I can run tests again, and guess what–it’ll fail. Cool. So let’s make it work, which is just two steps. First we need to add the button to counter.component.html:

<h1>Counter</h1>

<p>This is a simple example of an Angular component.</p>

<p aria-live="polite">Current count: <strong>{{ currentCount }}</strong></p>

<button class="btn btn-primary" (click)="incrementCounter()">Increment</button>

<button class="btn btn-secondary" (click)="decrementCounter()" id="decrement-button">Decrement</button>

And then we need to add the logic for when it’s clicked in counter.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'app-counter-component',
  templateUrl: './counter.component.html'
})
export class CounterComponent {
  public currentCount = 0;

  public incrementCounter() {
    this.currentCount++;
  }

  public decrementCounter() {
    this.currentCount--;
  }
}

That’s it. Now we can run ng test again and see that we now have 3 passing tests.

Good stuff. Now, let’s turn our attention to some untested code. FetchDataComponent is functional but has no tests for its display of data retrieved from the API. We’ll need to mock the API call to return some data and then assert that results are displayed as expected.

Here’s my fetch-data.component.spec.ts file with a test that mocks the API call and asserts that data is populated in the table on the UI:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

import { FetchDataComponent } from './fetch-data.component';

describe('FetchDataComponent', () => {
    let component: FetchDataComponent;
    let fixture: ComponentFixture<FetchDataComponent>;
    let httpMock: HttpTestingController;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [HttpClientTestingModule],
            declarations: [FetchDataComponent],
            providers: [{ provide: 'BASE_URL', useValue: 'https://my.fake.url/' }]
        }).compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(FetchDataComponent);
        component = fixture.componentInstance;
        httpMock = TestBed.get(HttpTestingController);
    });

    it('should retrieve weather forecasts', async(() => {
        const dummyForecasts = [
            {
                date: "date1",
                temperatureC: 0,
                temperatureF: 32,
                summary: "summary1"
            },
            {
                date: "date2",
                temperatureC: 100,
                temperatureF: 212,
                summary: "summary2"
            }
        ];

        const req = httpMock.expectOne('https://my.fake.url/weatherforecast');
        req.flush(dummyForecasts);

        expect(component.forecasts).toBe(dummyForecasts);
        fixture.detectChanges();
        const rowCount = fixture.nativeElement.querySelectorAll('table tr').length;
        expect(rowCount).toBe(3); // header plus two data rows
    }));
});

There’s a lot to unpack with this test, and I’m going to attempt to give a visual, summary-level explanation in the screenshot below.

Once again, we run tests using ng test and see that we now have passing tests for FetchDataComponent!

Advertisement

Author: Adam Prescott

I'm enthusiastic and passionate about creating intuitive, great-looking software. I strive to find the simplest solutions to complex problems, and I embrace agile principles and test-driven development.

Leave a comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: