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.
Top comments (5)
Could you write the Angular documentation here on out? This is excellent!
The people writing the Angular Docs are a lot smarter than me and they do a great job I feel!
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?
Very clear and useful article! Thank you for sharing ^^
This is perfect !