Following on from my previous post where I introduced unit testing Angular Components, this post will give a quick overview on practices I employ to unit test my services. In this post we will cover:
- Setting up a Service Test 💪
- Testing methods in the Service ðŸ›
- Mockng dependencies with Jasmine Spys 🔎
We will write some basic logic to handle a customer placing an order to illustrate the testing of the services involved.
Let's Get Started 🔥
Before we get into the fun part, we need to scaffold up a new Angular Project so that we can write and run our tests. Open your favourite Terminal or Shell at a new directory.
If you haven't already, I'd recommend installing the Angular CLI globally, it will be used frequently in this article: npm install -g @angular/cli
Now that we are in an empty directory, the first thing we'll want to do is set up an Angular project:
ng new test-demo
When it asks if you'd like to setup Angular Routing, type N, and when it asks which stylesheet format you would like to use, select any, it won't matter for this post.
Once the command has completed, you will need to navigate into the new project directory:
cd test-demo
We now have our basic app scaffold provided for us by Angular. Now we are going to want to set up some of the code that we'll be testing.
At this point, it's time to open your favourite Text Editor or IDE (I highly recommend VS Code).
Inside the src/app
directory, create a new directory and name it models
. We will create three files in here:
user.ts
export interface User {
id: string;
name: string;
}
product.ts
export interface Product {
id: string;
name: string;
cost: number;
}
order.ts
import { User } from './user';
import { Product } from './product';
export interface Order {
id: string;
user: User;
product: Product;
}
Once this is complete, we will use the Angular ClI to scaffold out two services:
ng g service services/user
and
ng g service services/order
These services will contain the logic that we will be testing. The Angular CLI will create these two files for us as well as some boilerplate testing code for each of the services. 💪
If we open order.service.spec.ts
as an example we will see the following:
import { TestBed } from '@angular/core/testing';
import { OrderService } from './order.service';
describe('OrderService', () => {
let service: OrderService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(OrderService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
Let's break that down a litte 🔨:
describe('OrderService', () => { ... })
sets up the Test Suite for the Order Service.
let service: OrderService
declares a Test Suite-scoped variable where we will store a reference to our Service.
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(OrderService);
});
This tells the test runner (Karma) to run this code before every test in the Test Suite. It is using Angular's TestBed
to create the testing environment and finally it is injecting the OrderService
and placing a reference to it in the service
variable defined earlier.
Note: if using Angular < v9 you may notice TestBed.get(OrderService)
rather than TestBed.inject(OrderService)
. They are essentially doing the same thing.
it('should be created', () => {
expect(service).toBeTruthy();
});
the it()
function creates a new test with the title should be created
. This test is expecting the service
varibale to truthy, in otherwords, it should have been instantiated correctly by the Angular TestBed. I like to think of this as the sanity check to ensure we have set up our Service correctly.
Service Logic Time 💡
Now that we have a basic understanding of what our Service Test file looks like, lets create some quick logic in our user.service.ts
and order.service.ts
file for us to test.
In user.service.ts
let's place the following code, which will store the active user in our app:
import { Injectable } from '@angular/core';
import { User } from '../models/user';
@Injectable({
providedIn: 'root'
})
export class UserService {
// Store the active user state
private activeUser: User;
constructor() {}
getActiveUser() {
// We'll return the active user or undefined if no active user
// The cast to Readonly<User> here is used to maintain immutability
// in our stored state
return this.activeUser as Readonly<User>;
}
setActiveUser(user: User) {
this.activeUser = user;
}
}
And in order.service.ts
let's create simple method to create an order:
import { Injectable } from '@angular/core';
import { Order } from './../models/order';
import { Product } from '../models/product';
import { UserService } from './user.service';
@Injectable({
providedIn: 'root'
})
export class OrderService {
constructor(private readonly userService: UserService) {}
createOrder(product: Product): Order {
return {
id: Date.now().toString(),
user: this.userService.getActiveUser(),
product
};
}
}
Awesome! We now have a nice little piece of logic that we can write some unit tests for.
Testing Time 🚀
Now for the fun part 💪 Let's get writing these unit tests. We'll start with UserService
as it is a more straightforward class with no dependencies.
Open user.service.spec.ts
and below the first test, we'll create a new test:
it('should set the active user correctly', () => {
// Arrange
const user: User = {
id: 'test',
name: 'test'
};
// Act
service.setActiveUser(user);
// Assert
expect(service['activeUser'].id).toEqual('test');
expect(service['activeUser'].name).toEqual('test');
});
In this test, we are testing that the user is set active correctly. So we do three things:
- Create a test user
- Call the
setActiveUser
method with our test user - Assert that the private
activeUser
property has been set with our test user.
Note: It is generally bad practice to access properties via string literals, however, in this testing scenario, we want to ensure correctness. We could have called the getActiveUser
method instead of accessing the private property directly, however, we can't say for certain if getActiveUser
works correctly at this point.
Next we want to test that our getActiveUser()
method is working as expected, so let's write a new test:
it('should get the active user correctly', () => {
// Arrange
service['activeUser'] = {
id: 'test',
name: 'test'
};
// Act
const user = service.getActiveUser();
// Assert
expect(user.id).toEqual('test');
expect(user.name).toEqual('test');
});
Again, we're doing three things here:
- Setting the current active user on the service
- Calling the
getActiveUser
method and storing the result in auser
variable - Asserting that the
user
returned is the active user we originally arranged
These tests are pretty straightforward, and if we run ng test
now we should see Karma reporting TOTAL: 7 SUCCESS
Awesome!! 🔥🔥
Testing with Mocks
Let's move onto a more complex test which involves having to mock out a dependency.
The first thing we are going to want to do is to mock out the call to the UserService
. We are only testing that OrderService
works correctly, and therefore, we don't want any ill-formed code in UserService
to break our tests in OrderService
.
To do this, just below the let service: OrderService;
line, add the following:
const userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['getActiveUser']);
And then inside the beforeEach
we want to change our TestBed.configureTestingModule
to match the following:
TestBed.configureTestingModule({
providers: [
{
provide: UserService,
useValue: userServiceSpy
}
]
});
Let me explain what's going on here. Jasmine creates an object identical to the UserService object, and the we override the Service being injected into the Testing Module with the spy object Jasmine created. (This is a technique centered around the Dependency Inversion principle).
Now we are able to change what is returned when our code calls userService.getActiveUser()
to allow us to perform multiple test cases. We will see that in action now when we write our test for the OrderService
:
it('should create an order correctly', () => {
// Arrange
const product: Product = {
id: 'product',
name: 'product',
cost: 100
};
userServiceSpy.getActiveUser.and.returnValue({ id: 'test', name: 'test' });
// Act
const order = service.createOrder(product);
// Assert
expect(order.product.id).toEqual('product');
expect(order.user.id).toEqual('test');
expect(userServiceSpy.getActiveUser).toHaveBeenCalled();
});
We are doing 5 things in this test:
- Creating the product that the user will order
- Mocking out the response to the
getActiveUser
call to allow us to set up a test user - Calling the
createOrder
method with our test product - Asserting that the order was indeed created correctly
- Asserting that the
getActiveUser
method onUserService
was called
And now, if we run ng test
again, we will see 8 tests passing!
With just these few techniques, you can go on to write some pretty solid unit tests for your services! 🤓
Your team, and your future self, will thank you for well tested services!
This is a short brief non-comprehensive introduction into Unit Testing Services with Angular with Jasmine and Karma.
If you have any questions, feel free to ask below or reach out to me on Twitter: @FerryColum.
Top comments (1)
Hey, this article really helped me a lot for testing services. I would LOVE to see unit testing for ReactiveForms !
Thank you Colum Ferry