Writing unit test cases with Karma for angular component

August 07, 2020

I would like to convince you to start writing unit test cases for your angular web app. Shorten time to production is not an excuse to accumulate technical debts. There are many good reasons why you should start writing it:

  1. It helps you identify issues as soon as possible, especially when many different teams working on the same code base and breaking each other changes. It’s not fun to get an urgent call at mid-night for production support, then realized a bug was released to production.
  2. It helps you write better code as you can refractor with confidence that your app still working as expected. You can think and break down your code into small units, which is more manageable than a monolithic system.
  3. It may be simply your company policy which requires you to get a code coverage of 80% or above.

No matter the reasons, you need to know-how as well, as I notice that many new developers have no idea nor intention on doing it. If you are working on angular io project, it’s very easy to get started. Simply run in your project:

npm run test

Then a Chrome browser is open at localhost with port 9876.

0  5Z4Bah2d6yWaIxEQ

Clicked on the “Debug” button and you can start testing:

0  Ylm4FdbK0G0h5O6v

Right now nothing get runs as we haven’t started writing our test case yet. We can start writing cases to cover small isolated pieces of code, such as your method call. Don’t rely on manual testing to discover problems in your code as it’s your responsibility as a developer for code quality.

Let’s say, for example, you have a login.component.ts, which has a logon method. After it’s triggered, it would update a boolean flag from false to true:

export class LoginComponent {

  isLogon = false;

  login() {
      this.isLogon = true;
  }
}

Then you can create a file with name login.component.spec.ts, which would be used for our test cases. After that, write your first test case:

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

import { LoginComponent } from './login.component';

describe('LoginComponent', () => {
  let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ LoginComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should able to logon', () => {
    component.login();
    expect(component.isLogon).toBeTruthy();
  })
});

The test suits are inside your describe ( ) and each test case we wrote is inside the it ( ), and expect the isLogon flag is true after the logon method is triggered. Don’t worry about the rest, for now, restart the Chrome browser:

0  cIksYbETGFONqknt

Your first test case is passed! Later on, in case some new joiner developer changed your code and modify it as below:

export class LoginComponent {

  isLogon = false;

  login() {
      this.isLogon = false;
  }
}

You would be able to see your test case failed:

0  zbYhLW9qdjZ70WbL

Okay, the above test case is trivial. In real world, we often make an API call to fetch some data from server. Remember, don’t call the real API to fetch data in your test case! Always mock your API call with stub data returned instead. To see how it works, let’s rewrite our simple logon component with service call:

import { AuthenticationService } from '../../services/authentication.service';

export class LoginComponent {

  constructor(private authenticationService: AuthenticationService { }

  isLogon = false;

  login() {

    this.authenticationService.login().subscribe(
                data => {
                  this.isLogon = true;
                },
                error => {
                  this.isLogon = false;
                });
  }
}

We injected an authentication service into our component, and call the API, if it’s a success, it would update the isLogon boolean flag to true.

Now, let’s re-run our test, you would hit error, because we haven’t injected the service to our testing component yet. We can re-write our test case like this:

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

import { LoginComponent } from './login.component';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';

let stubData = {
  'username': 'testing'
};

class FakeAuthenticationService {

  login(){
    return Observable.of(stubData)
  }
}

describe('LoginComponent', () => {
  let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;

  let newFakeAuthenticationService = new FakeAuthenticationService();

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ LoginComponent ],
      providers: [
        AuthenticationService
      ]
    })
    TestBed.overrideProvider(AuthenticationService, {useValue: newFakeAuthenticationService});
    TestBed.compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should able to logon', () => {
    component.login();
    expect(component.isLogon).toBeTruthy();
  })
});

Here we add the provider Authentication Service at the configuration. Then we override the AuthenticationService with our fake AuthenticationService. The fake AuthenticationService simply return an Observable of stub data. Try to run our test case again and it passed!

0  xLndWBjQlMKCEKkg

This is just a demo case and there are better ways to write it, such as Jasmine createSpyObj and HttpClientTestingModule. But for simplicity, I just want to convince you to start writing unit test cases instead of scare you away.

Originally published at https://victorleungtw.com on August 7, 2020.


Written by Victor Leung who is a keen traveller to see every country in the world, passionate about cutting edge technologies. Follow me on Twitter