Loading...

Angular - Unit testing with Jasmine and Karma

Why Testing?

During my years as a consultant, I passed a lot of companies, and I have to say that almost all the companies I saw had none or poor testing in the frontend section of the app.
When I asked developers why they don't write tests for their code, the answer is usually the same, "NO TIME". The thing is that if you know how to write tests properly, writing them is doing just the oposite, it will save you time.
Writing a unit test after you know what you are doing is a really quick task, the first set up of the test might take a bit longer but after that first bump on the road adding additional tests is done really quickly. It will probably be quicker to write the test and run it then to run the steps in the browser to get to the bug you are trying to fix.
A bug is never destroyed unless it's properly tested. Did you find a bug on the app? fixed that bug? that bug can return unless you write test to ensure it will not, so either you will have to manually test the issue every time before deploy or you can write a test and have a good CI deployment process to run tests automatically before deploy.
So testing will also prevent the bug from returning and will save you time to check the bug every time before releasing a new version, testing combined with CI will also ensure a better quality product for your users.

Bootstrapping angular for testing

Starting a project with @angular/cli will already get you up and running with unit testing. Let's examine what angular cli install for us.

  • Karma - karma is a test runner that will launch a browser and run the tests on the browser
  • Jasmine - behavior driven development framework for testing, we use jasmine to write our unit testing

Let's go over the installed tools

Karma

Karma's job is to launch a browser, and run our test files in that browser.
It knows how to do it's job by looking at a configuration file which by default will be named: karma.conf.js (karma will know it's a configuration file based on the name convention of the file).
@angular/cli already created a default configuration file located in each project we create.
Also each project contains a test.ts file which is the entry point for karma to launch. In angular.json for each project there is a configuration regarding the test in projects.<project-name>.architect.test where you can specify the entry point file for the test and the karma configuration file.
As for the configuration the main things in it are:

  • browsers - you can declare in this array which browsers will run the tests, need to place an appropriate plugin for each browser, default is set to chrome
  • plugins - used to extend karma functionality, you can set karma to output reports, to be able to launch chrome of firefox, to be able to use webpack and more
  • frameworks - determine to what testing framework will karma be connected, by default it's set to jasmine.
  • singleRun - whether to run one time or keep it running and rerun the tests when files change
Jasmine

We use Jasmine to write our tests, Jasmine is a test framwork where you describe what you are testing and the desired behavior. You group tests together with describe, you place a single test in a it block, you can run logic before each test or before a group of tests are running.
Let's go over the common Jasmine features we will use

describe

create a group of tests. for example if you are creating an angular component called HelloWorldComponent, and you would like to create unit testing for that component, you might have a describe like so:

        
            describe('HelloWorldComponent', () => {
                ... the rest of the tests
            })
        
    

Groups can also be nested within other groups. So for example if the HelloWorldComponent is a ControlValueAccessor and we want to test that we can use the control with ngModel directive, we might have a group of ng model tests in our component test, so the structure might look like so

        
            describe('HelloWorldComponent', () => {
                describe('ngModel', () => {
                    ... tests related to control value accessor
                })
            })
        
    
befores

Two functions in this group beforeEach, beforeAll.

  • beforeEach - called in a describe block, will run a function before each of the tests in the describe block.
    will run them based on the order they are declared.
  • beforeAll - called in a describe block, will run a function one time before the first test of the describe block runs

for example let's say before each test of the HelloWorldComponent we will want to create a new component. It will look something similar to this:

        
            describe('HelloWorldComponent', () => {
                beforeEach(() => {
                    ...create new HelloWorldComponent
                })
            })
        
    
it

define a single test, a test should have atleast a single expect for a desired test result. So testing our HelloWorldComponent might look like this

        
            describe('HelloWorldComponent', () =&gt; {
                beforeEach(() =&gt; {
                    ...create new HelloWorldComponent
                })
            
                describe('ControlValueAccessor', () => {
                    it('placing ngModel on component two way binding should work', () => {
                         ... the code for the test
                    })
                })
            })
        
    
expect

This is used to assert that a certain element or value is equal in the test to what it is supposed to be. we place expects inside our tests meaning in our it blocks.
So this might be similar to this

        
            describe('HelloWorldComponent', () =&gt; {
                beforeEach(() =&gt; {
                    ...create new HelloWorldComponent
                })
            
                describe('ControlValueAccessor', () => {
                    it('placing ngModel on component two way binding should work', () => {
                         ... the code for the test
                         expect(<some value>).toBe('stam');
                    })
                })
            })
        
    
spyOn

A spy can check if a function is called, can check what arguments where passed to a method, and stub a method return value or delegate to the original.
A spy can exist in a describe block or an it block and will be removed after the block exited.
The syntax of a spy might be

        
            describe('HelloWorldComponent', () =&gt; {
                beforeEach(() =&gt; {
                    helloWorldComponent = ...create new HelloWorldComponent
                    spyOn(helloWorldComponent, 'sayHello')
                })
            
                describe('ControlValueAccessor', () => {
                    it('placing ngModel on component two way binding should work', () => {
                         ... the code for the test
                        expect(helloWorldComponent.sayHello.calls.count()).toEqual(2);
                    })
                })
            })
        
    
Our first angular test

Let's start by creating a simple test for an hello world component.
Let's create a simple component with an hello world message that looks like this:

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

            @Component({
            selector: 'app-hello-world',
            templateUrl: './hello-world.component.html',
            styleUrls: ['./hello-world.component.css']
            })
            export class HelloWorldComponent implements OnInit {

            constructor() { }

            ngOnInit() {
            }

            }
        
    

and the template for that component is

        
            <p>
            hello-world works!
            </p>
        
    

Let's write a test that makes sure that the text of the component is: hello-world works

Angular cli already configures karma and jasmine in our workspace, and it will run files that end with .spec.ts.
If creating a component using the angular cli, then we will see also a test file for our component is created for us.
The test file that is created for us contains a few sections

describe

We have to place our test in a describe block so our test file will looks like this

        
            describe('HelloWorldComponent', () => {
                ... test code
            });
        
    
TestingModule

a testing module represents the minimum module we have to define in order to test our component.
We want the creation of this module to happen before each test so we will create it in the beforeEach in our describe block
We will use TestBed which is a class used to configure and interact with the testing module. configuring the module for our component might look like this:

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

Our minimum module just contain the component we are testing, but usually it will have additional services and modules the component is using, since our component is simple we just have the component defined in our testing module.

Creating the test subject

The next step in our test configuration is to create the test subject, or in this case creating the component we want to test, beforeEach test is running.
Creating our component might look like this:

        
            describe('HelloWorldComponent', () => {
                let fixture: ComponentFixture<HelloWorldComponent>;
                
                beforeEach(() => {
                  fixture = TestBed.createComponent(HelloWorldComponent);
                  fixture.detectChanges();
                });
              ...
        
    

So we are using TestBed to create our component and place it in a variable so we can refrence the component on each test.

Create our test

Time to create the actual test, we want to test the text in the p tag of the component is matching hello-world works! which it should obviously pass, but let's write the test just for practice reasons.

        
            it('should create', () => {
                const p = fixture.debugElement.query(By.css('p'));
                expect(p.nativeElement.innerText).toBe('hello-world works!');
            });
        
    

The debugElement represents the root host of the component, and we can use the query to select elements in our component
We can then check the dom elements if it meets our expectation.
We can run the test with the command

        
            > ng test unit-testing-tutorial
        
    
Testing forms

let's try and expend our component and test to a much useful test case. let's say our component has a search form, and when submitted it calls a method on the class.
We want to create a test that place a string in the form, make sure two way binding is working and the text is moved to our class public property, and also make sure that when submitting the form the proper method is being called.
Our new component might look like this:

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

            @Component({
            selector: 'app-hello-world',
            templateUrl: './hello-world.component.html',
            styleUrls: ['./hello-world.component.css']
            })
            export class HelloWorldComponent {
                searchString = '';
                constructor() { }

                searchFormSubmitted = () => {
                    console.log('search form submitted');
                }
            }

        
    

And the template of the component might look like so:

        
            <form (ngSubmit)="searchFormSubmitted()">
                <label>Search</label>
                <input type="search" name="search" [(ngModel)]="searchString" />
                <button type="submit">Search</button>
            </form>
        
    

So let's try and write the test for this component, first we will test changing the text input and making sure that the public property connected to the ng model directive is updated with the new value.

        
            it('should create', () => {
                const searchInput = fixture.debugElement.query(By.css('input[name="search"]'));
                searchInput.nativeElement.value = 'nerdeez';
                searchInput.nativeElement.dispatchEvent(new Event('input'));
                fixture.detectChanges();
                expect(fixture.componentInstance.searchString).toBe('nerdeez');
            });
        
    

Now if you try and run your test, you would notice that the test is failing, we can debug our tests by clicking the debug button on the top right of the screen, and on the next screen the opens we can open the developer tools and place breakpoints where we want and refresh the page when we are ready.
For one reason we have to change our testing module to include FormModule since we are using directives from there in our component, so change the before each of the testing module creation to this:

        
            beforeEach(async(() => {
                TestBed.configureTestingModule({
                    declarations: [ HelloWorldComponent ],
                    imports: [FormsModule]
                })
                .compileComponents();
            }));
        
    

But even after this change the test is still not passing.
The reason for this is that template driven forms that are using ngForm and ngModel directives will tak longer then a single tick to hold the values and update the public properties.
We can tell angular to continue only after all async tasks are finished by using the async so we will cover the creation of the component in async and make sure each test is running after the ngModel and ngForm is set up and there are no pending async tasks left.
cover the creating component like so:

        
            beforeEach(async(() => {
                fixture = TestBed.createComponent(HelloWorldComponent);
                fixture.detectChanges();
            }));
        
    

and now our test is passing, let's add a spy on the method to verify that our method is called.
Change the test to the following:

        
            it('should create', () => {
                const searchInput = fixture.debugElement.query(By.css('input[name="search"]'));
                searchInput.nativeElement.value = 'nerdeez';
                searchInput.nativeElement.dispatchEvent(new Event('input'));
                fixture.detectChanges();
                expect(fixture.componentInstance.searchString).toBe('nerdeez');
            
                spyOn(fixture.componentInstance, 'searchFormSubmitted');
                const form = fixture.debugElement.query(By.css('form'));
                form.nativeElement.dispatchEvent(new Event('submit'));
                fixture.detectChanges();
                expect(fixture.componentInstance.searchFormSubmitted).toHaveBeenCalled();
            });
        
    

So what we did here is place a spy on the method that we want to check that is invoked, with the spy we can trace if the method is called. We grabbed the form and dispatched a submit event and verified that our method is called.

Testing server communication

Let's go for a different scenario. Let's test a component that query a server for data. Our component will grab a list of todo tasks from a server and display them on screen.
So our component might look something like this:

        
            import { Component, OnInit } from '@angular/core';
            import { HttpClient } from '@angular/common/http';
            import { Observable } from 'rxjs';

            @Component({
            selector: 'app-hello-world',
            templateUrl: './hello-world.component.html',
            styleUrls: ['./hello-world.component.css']
            })
            export class HelloWorldComponent implements OnInit {
                public tasks$: Observable<any>;

                constructor(private _httpClient: HttpClient) { }

                ngOnInit() {
                    this.tasks$ = this._httpClient.get('https://nztodo.herokuapp.com/api/task/?format=json');
                }
            }
        
    

and the template of the component might look like this

        
            <ul>
                <li *ngFor="let task of (tasks$ | async)">
                    {{task.title}}
                </li>
            </ul>
        
    

So let's test our component, we don't want our tests to depend on a server so it's recommended to not place server calls on unit testing. So we will have to mock the server response.
We will mock the server as returning 4 elements and make sure that we have 4 lis in our component.
Since we are using a dependency injector in angular, we can easily instruct the DI to supply our service when asked for HttpClient.
Modify the beforeEach where we configured the testing module to look like this:

        
            const mockHttpClient = {
                get: (url) => of([
                  {id: 1, title: 'hello'},
                  {id: 2, title: 'world'},
                  {id: 3, title: 'foo'},
                  {id: 4, title: 'bar'},
                ])
              }
              beforeEach(async(() => {
                TestBed.configureTestingModule({
                  declarations: [ HelloWorldComponent ],
                  imports: [FormsModule],
                  providers: [
                    {provide: HttpClient, useValue: mockHttpClient}
                  ]
                })
                .compileComponents();
              }));
        
    

So our test might look like so

        
            it('should place 4 lis', () => {
                const lis = fixture.debugElement.queryAll(By.css('li'));
                expect(lis.length).toBe(4);
            })
        
    

we are just grabbing all the lis and making sure there are 4 of them. We can use a similar technique to mock different angular services not just HttpClient

Testing a service

Testing a service is much simpler then component. When creating a new service with angular cli, you will see a test is created for you.
Inside the it test function you can ask to inject a service from the module using the inject method

        
            it('should be created', inject([HelloService], (service: HelloService) => {
                expect(service).toBeTruthy();
            }));
        
    

After injecting the service we can now write a test on service methods.

Summary

Angular made writing unit tests a breeze, and after getting used to the tools you have, it will be faster then checking everything in the browser