Unit Testing Angular - Services

coly010

Colum Ferry

Posted on March 29, 2020

Unit Testing Angular - Services

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;
}
Enter fullscreen mode Exit fullscreen mode

product.ts

export interface Product {
  id: string;
  name: string;
  cost: number;
}
Enter fullscreen mode Exit fullscreen mode

order.ts

import { User } from './user';
import { Product } from './product';

export interface Order {
  id: string;
  user: User;
  product: Product;
}
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

Again, we're doing three things here:

  • Setting the current active user on the service
  • Calling the getActiveUser method and storing the result in a user 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']);
Enter fullscreen mode Exit fullscreen mode

And then inside the beforeEach we want to change our TestBed.configureTestingModule to match the following:

TestBed.configureTestingModule({
  providers: [
    {
      provide: UserService,
      useValue: userServiceSpy
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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 on UserService 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.

💖 💪 🙅 🚩
coly010
Colum Ferry

Posted on March 29, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related