loading...
Cover image for Unit Testing Angular - Component Testing

Unit Testing Angular - Component Testing

coly010 profile image Colum Ferry ・10 min read

Everyday we are seeing a bigger push towards adding automated tests to our apps. Whether these are unit tests, integration or e2e tests.

This will be a series of articles based on writing unit tests for Angular and some of it's core concepts: Components, Services, Pipes and Guards.

These articles are not intended to be comprehensive, rather a soft introduction to unit testing. For more detailed component testing documentation, Angular has a great docs page here: https://angular.io/guide/testing

It's worth noting that some of my opinionated approaches to testing will come through in this article. Testing is a very opinated topic already. My advice to look through all the testing strategies that are out there and make decide what you think is the best approach.

In this article, we will explore testing components, ranging from simple to more complex components and we will cover the following:

  • What is a unit test? πŸ’‘
  • Why write unit tests? πŸ€”
  • Ok, now how do we write unit tests? πŸ˜„

We will be using the standard Jasmine and Karma testing setup that Angular provides out of the box on apps generated with the Angular CLI.

πŸ’‘ What is a unit test?

A unit test is a type of software testing that verifies the correctness of an isolated section (unit) of code.

Lets say you have a simple addition function:

function sum(...args) {
    return args.reduce((total, value) => total + value, 0);
}

This full function can be considered a unit, and therefore your test would verify that this unit is correct. A quick test for this unit could be:

it('should sum a range of numbers correctly', () => {
    // Arrange
    const expectedValue = 55;
    const numsToTest = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Act
    const total = sum(...numsToTest);

    // Assert
    expect(total).toBe(expectedValue);
});

We're introducting a few concepts here.
The it(...args) is the function that will set up our unit test. It's pretty common testing terminology across Test Runners.

We also introduce the AAA Test Pattern. It's a pattern that breaks your test into 3 sections.

The first section is Arrange: Here you perform any set up required for your test.

The second section is Act: Here you will get your code to perform the action that you are looking to test.

The third and final sction is Assert: Here you will make verify that the unit performed as expected.

In our test above we set what we are expecting the value to be if the function performs correctly and we are setting the data we will use to test the function.

We then call the sum() function on our previously arranged test data and store the result in a total variable.

Finally, we check that the total is the same as the value we are expecting.

If it is, the test will pass, thanks to us using the expect() method.

Note: .toBe() is a matcher function. A matcher function performs a check that the value passed into the expect() function matches the desired outcome. Jasmine comes with a lot of matcher functions which can be viewed here: Jasmine Matchers

πŸ€” But Why?

Easy! Confidence in changes.

As a developer, you are consistently making changes to your codebase. But without tests, how do you know you haven't made a change that has broken functionality in a different area within your app?

You can try to manually test every possible area and scenario in your application. But that eats into your development time and ultimately your productivity.

It's much more efficient if you can simply run a command that checks all areas of your app for you to make sure everything is still functioning as expected. Right?

That's exactly what automated unit testing aims to achieve, and although you spend a little bit more time developing features or fixing bugs when you're also writing tests, you will gain that time back in the future if you ever have to change functionality, or refactor your code.

Another bonus is that any developer coming along behind you can use the test suites you write as documentation for the code you write. If they don't understand how to use a class or a method in the code, the tests will show them how!

It should be noted, these benefits come from well written tests. We'll explore the difference between a good and bad test later.

πŸ˜„ Let's write an Angular Component Test

We'll break this down into a series of steps that will cover the following testing scenarios:

  • A simple component with only inputs and outputs
  • A complex component with DI Providers

Let's start with a simple component that only has inputs and outputs. A purely presentational component.

πŸ–ΌοΈ Presentational Component Testing

We'll start with a pretty straight forward component user-speak.component.ts that has one input and one output. It'll display the user's name and have two buttons to allow the user to talk back:

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
    selector: 'app-user-speak',
    template: `
        <div>Hello {{ name }}</div>
        <div>
            <button (click)="sayHello()">Say Hello</button>
            <button (click)="sayGoodbye()">Say Goodbye</button>
        </div>
    `
})
export class UserSpeakComponent {
    @Input() name: string;
    @Output() readonly speak = new EventEmitter<string>();

    constructor() {}

    sayHello() {
        this.speak.emit('Hello');
    }

    sayGoodbye() {
        this.speak.emit('Goodbye');
    }
}

If you used the Angular CLI (highly recommended!) to generate your component you will get a test file out of the box. If not, create one user-speak.component.spec.ts.

Note: the .spec.ts is important. This is how the test runner knows how to find your tests!

Then inside, make sure it looks like this initially:

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

import { UserSpeakComponent } from './user-speak.component';

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

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

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

    it('should create', () => {
        expect(component).toBeTruthy();
    });
});

Let's explain a little of what is going on here.

The describe('UserSpeakComponent', () => ...) call is setting up a Test Suite for our User Speak Component. It will contain all the tests we wish to perform for our Component.

The beforeEach() calls specify code that should be executed before every test runs. With Angular, we have to tell the compile how to interpret and compile our component correctly. That's where the TestBed.configureTestingModule comes in. We will not go into too much detail on that for this particular component test, however, later in the article we will describe how to change it to work when we have DI Providers in our component.

For more info on this, check out the Angular Testing Docs

Each it() call creates a new test for the test runner to perform.

In our example above we currently only have one test. This test is checking that our component is created successfully. It's almost like a sanity check to ensure we've set up TestBed correctly for our Component.

Now, we know our Component class has a constructor and two methods, sayHello and sayGoodbye. As the constructor is empty, we do not need to test this. However, the other two methods do contain logic.

We can consider each of these methods to be units that need to be tested. Therefore we will write two unit tests for them.

It should be kept in mind that when we do write our unit tests, we want them to be isolated. Essentially this means that it should be completely self contained. If we look closely at our methods, you can see they are calling the emit method on the speak EventEmitter in our Component.

Our unit tests are not interested in whether the emit functionality is working correctly, rather, we just want to make sure that our methods call the emit method appropriately:

it('should say hello', () => {
    // Arrange
    const sayHelloSpy = spyOn(component.speak, 'emit');
    // Act
    component.sayHello();
    // Assert
    expect(sayHelloSpy).toHaveBeenCalled();
    expect(sayHelloSpy).toHaveBeenCalledWith('Hello');
});

it('should say goodbye', () => {
    // Arrange
    const sayGoodbyeSpy = spyOn(component.speak, 'emit');
    // Act
    component.sayGoodbye();
    // Assert
    expect(sayGoodbyeSpy).toHaveBeenCalled();
    expect(sayGoodbyeSpy).toHaveBeenCalledWith('Goodbye');
});

Here we meet the spyOn function which allows us to mock out the actual implementation of the emit call, and create a Jasmine Spy which we can then use to check if the emit call was made and what arguments were passed to it, thus allowing us to check in isolation that our unit performs correctly.

If we run ng test from the command line, we will see that the tests pass correctly. Wonderful.

πŸ”§ REFACTOR

Hold up! Having two methods that essentially do the same thing is duplicating a lot of code. Let's refactor our code to make it a bit more DRY:

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
    selector: 'app-user-speak',
    template: `
        <div>Hello {{ name }}</div>
        <div>
            <button (click)="saySomething('Hello')">Say Hello</button>
            <button (click)="saySomething('Goodbye')">Say Goodbye</button>
        </div>
    `
})
export class UserSpeakComponent {
    @Input() name: string;
    @Output() readonly speak = new EventEmitter<string>();

    constructor() {}

    saySomething(words: string) {
        this.speak.emit(words);
    }
}

Awesome, that's much nicer. Let's run the tests again: ng test.

Uh Oh! 😱

Tests are failing!

Our unit tests were able to catch correctly that we changed functionality, and potentially broke some previously working functionality. πŸ’ͺ

Let's update our tests to make sure they continue to work for our new logic:

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

import { UserSpeakComponent } from './user-speak.component';

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

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

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

    it('should create', () => {
        expect(component).toBeTruthy();
    });

    it('should say something', () => {
        // Arrange
        const saySomethingSpy = spyOn(component.speak, 'emit');

        // Act
        component.saySomething('something');

        // Assert
        expect(saySomethingSpy).toHaveBeenCalled();
        expect(saySomethingSpy).toHaveBeenCalledWith('something');
    });
});

We've removed the two previous tests and updated it with a new test. This test ensures that any string that is passed to the saySomething method will get passed on to the emit call, allowing us to test both the Say Hello button and the Say Goodbye.

Awesome! πŸš€

Note: There is an argument around testing JSDOM in unit tests. I'm against this approach personally, as I feel it is more of an integration test than a unit test and should be kept separate from your unit test suites.

Let's move on:

🀯 Complex Component Testing

Now we have seen how to test a purely presentational component, let's take a look at testing a Component that has a DI Provider injected into it.

There are a few approaches to this, so I'll show the approach I tend to take.

Let's create a UserComponent that has a UserService injected into it:

import { Component, OnInit } from '@angular/core';
import { UserService } from '../user.service';

@Component({
    selector: 'app-user',
    template: `
        <app-user-speak
            [name]="user?.name"
            (speak)="onSpeak($event)"
        ></app-user-speak>
    `
})
export class UserComponent implements OnInit {
    user: User;

    constructor(public userService: UserService) {}

    ngOnInit(): void {
        this.user = this.userService.getUser();
    }

    onSpeak(words: string) {
        console.log(words);
    }
}

Fairly straightforward except we have injected the UserService Injectable into our Component.

Again, let's set up our intial test file user.component.spec.ts:

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

import { UserComponent } from './user.component';

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

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

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

    it('should create', () => {
        expect(component).toBeTruthy();
    });
});

If we were to run ng test now, it would fail as we are missing the Provider for the UserService therefore TestBed cannot inject it correctly to create the component successfully.

So we have to edit the TestBed set up to allow us to create the component correctly. Bear in mind, we are writing unit tests and therefore only want to run these tests in isolation and do not care if the UserService methods are working correctly.

The TestBed also doesn't understand the app-user-speak component in our HTML. This is because we haven't added it to our declarations module. However, time for a bit of controversy. My view on this is that our tests do not need to know the make up of this component, rather we are only testing the TypeScript within our Component, and not the HTML, therefore we will use a technique called Shallow Rendering, which will tell the Angular Compiler to ignore the issues within the HTML.

To do this we have to edit our TestBed.configureTestingModule to look like this:

TestBed.configureTestingModule({
    declarations: [UserComponent],
    schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

That will fix our app-user-speak not declared issue. But we still have to fix our missing provider for UserService error. We are going to employ a technique in Unit Testing known as Mocking, to create a Mock Object, that will be injected to the component instead of the Real UserService.

There are a number of ways of creating Mock / Spy Objects. Jasmine has a few built in options you can read about here.

We are going to take a slightly different approach:

TestBed.configureTestingModule({
    declarations: [UserComponent],
    providers: [
        {
            provide: UserService,
            useValue: {
                getUser: () => ({ name: 'Test' })
            }
        }
    ],
    schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

The part we are interested in now is our providers array. Here we are telling the compiler to provide the value defined here as the UserService. We set up a new object and define the method we want to mock out, in this case getUser and we will tell it a specific object to return, rather than allowing the real UserSerivce to do logic to fetch the user from the DB or something similar.

My thoughts on this are that every Public API you interact with should have be tested and therefore your unit test doesn't need to ensure that API is working correctly, however, you want to make sure your code is working correctly with what is returned from the API.

Now let's write our test to check that we are fetching the user in our ngOnInit method.

it('should fetch the user', () => {
    // Arrange
    const fetchUserSpy = spyOn(
        component.userService,
        'getUser'
    ).and.returnValue({ name: 'Test' });

    // Act
    component.ngOnInit();

    // Assert
    expect(fetchUserSpy).toHaveBeenCalled();
});

Here we simply create a spy to ensure that the getUser call is made in the ngOnInit methoid. Perfect.

We also leverage the .and.returnValue() syntax to tell Jasmine what it should return to the ngOnInit() method when that API is called. This can allow us to check for edge cases and error cases by forcing the return of an error or an incomplete object.

Let's modify our ngOnInit() method to the following, to allow it to handle errors:

ngOnInit(): void {
    try {
      this.user = this.userService.getUser();
    } catch (error) {
      this.user = null;
    }
  }

Now let's write a new test telling Jasmine to throw an error, allowing us to check if our code handles the error case correctly:

it('should handle error when fetching user', () => {
    // Arrange
    const fetchUserSpy = spyOn(component.userService, 'getUser').and.throwError(
        'Error'
    );

    // Act
    component.ngOnInit();

    // Assert
    expect(fetchUserSpy).toHaveBeenCalled();
    expect(fetchUserSpy).toThrowError();
    expect(component.user).toBe(null);
});

Perfect! πŸ”₯πŸ”₯ We are now also able to ensure our code is going to handle the Error case properly!


This is a short brief non-comprehensive introduction into Unit Testing Components with Angular with Jasmine and Karma. I will be publishing more articles on Unit Testing Angular which will cover testing Services, Data Services, Pipes and Guards.

If you have any questions, feel free to ask below or reach out to me on Twitter: @FerryColum.

Discussion

pic
Editor guide
Collapse
zacharythomasstone profile image
Zachary Stone

Could you write the Angular documentation here on out? This is excellent!

Collapse
coly010 profile image
Colum Ferry Author

The people writing the Angular Docs are a lot smarter than me and they do a great job I feel!

Collapse
danoswalt profile image
Dan Oswalt

The docs say compileComponents is not necessary if running the test via the cli. I tend to remove it, but is there a compelling reason to leave it in there?