Testing Reactive Service with a Subject in Angular

The Service with a Subject pattern in Angular is a good way to get started with reactive programming (ie. making use of RxJS) in Angular. The following article describes how to get started with the pattern: Getting Started with Service with a Subject in Angular.

The next step after implementing the Service with a Subject pattern is to write tests for the service. These tests will involve working with observables. Testing observables with plain Jasmine leads to tests with logic not related to verifying that the service works. The jasmine-marbles library includes several methods that help test observables.

The Service with a Subject Under Test

For reference, the following is the service that needs to be tested.
name.service.ts

// name.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class NameService {
  private mName$ = new BehaviorSubject<string>(null);

  name$(): Observable<string> {
    return this.mName$;
  }

  constructor(private httpClient: HttpClient) { }

  loadName() {
    this.httpClient.get<string>('name URL')
      .subscribe(
        name => this.mName$.next(name),
        () => null
      );
  }
}

To figure out what needs to be tested, let’s think about what could go wrong. There are 3 main things that are likely to go wrong. The first is that the incorrect URL is passed to the get function of the HttpClient. The second is that the return value of the HTTP GET request is not passed to the subject correctly. The third is that, if the HTTP GET request returns an error, an exception is raised.

Service with a Subject Tests

The following is the test code that checks the 3 things that are likely to go wrong described earlier.
name.service.spec.ts

// name.service.spec.ts
import { HttpClient } from '@angular/common/http';
import { cold } from 'jasmine-marbles';

import { NameService } from './name.service';

describe('NameService', () => {
  let service: NameService;
  let httpClientSpy: jasmine.SpyObj<HttpClient>;

  beforeEach(() => {
    httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
    service = new NameService(httpClientSpy);
  });

  describe('loadName', () => {
    it('should call get on HttpClient with correct URL', () => {
      // Setting up get spy
      httpClientSpy.get.and.returnValue(cold('a|', {a: null}));

      // Calling loadName
      service.loadName();

      // Checking get call
      expect(httpClientSpy.get).toHaveBeenCalledWith('name URL');
    });

    it('should emit new name on name$', () => {
      // Defining marbles
      const getSpyMarbles =   '-a|';
      const expectedMarbles = 'bc';
      // Setting up get spy
      httpClientSpy.get.and.returnValue(cold(getSpyMarbles, {a: 'name'}));

      // Calling loadName
      service.loadName();

      // Checking get call
      const expected = cold(expectedMarbles, {b: null, c: 'name'});
      expect(service.name$()).toBeObservable(expected);
    });

    it('should emit nothing on name$ if an error occurs', () => {
      // Defining marbles
      const getSpyMarbles =   '-#|';
      const expectedMarbles = 'a';
      // Setting up get spy
      httpClientSpy.get.and.returnValue(cold(getSpyMarbles));

      // Calling loadName
      service.loadName();

      // Checking get call
      const expected = cold(expectedMarbles, {a: null});
      expect(service.name$()).toBeObservable(expected);
    });
  });
});

Test Setup

The setup common to all tests is on lines 8-14. Lines 8 and 9 define the service under test and the HttpClient spy used for the tests that are initialised on lines 11-14. The three test scenarios are defined on lines 17-26 (checking the URL passed to the HttpClient get function), 28-41 (checking that the return value of the HTTP GET request is passed on) and 43-56 (checking the behaviour of the service when the HTTP GET returns an error).

GET Request URL

The test on lines 17-26 checks that the correct URL is passed to the get function of the HttpClient. The first step on line 19 is to give the get call a return value. In this case we only care about what is passed to the get function, so what is set as the return value is irrelevant as long as it is an observable. Line 22 triggers the function that triggers the GET request. Line 25 checks that the get function was called with the correct URL.

Return Value is Passed On

The test on line 28-41 checks that the HTTP GET request return value is passed on by the name$ function as an observable. Lines 30 and 31 define marbles that control when the HTTP GET request emits a value and when the name$ function should emit a value, respectively. Line 33 defines what the HTTP GET request returns.

Line 36 triggers the function that triggers the HTTP GET request. Line 39 defines that value that is expected to be emitted by the name$ function (being the null with which the BehaviourSubject is initialised and the GET request return value) and line 40 checks for that expectation.

HTTP GET Request Error

The test on lines 43-56 checks the behaviour of the code if the HTTP GET request returns an error. Lines 45 and 46 define marbles that control when the HTTP GET request returns an error (indicated by #) and when the name$ function should emit a value, respectively. Line 48 defines what the HTTP GET request should return.

Line 51 triggers the function that triggers the HTTP GET request. Line 54 defines that value that is expected to be emitted by the name$ function (being only the null with which the BehaviourSubject is initialised) and line 55 checks for that expectation.

These tests are the base on which tests can be built if the logic in the service becomes more complicated. Examples of additional complications are if the HTTP GET return value needs to be transformed or if multiple HTTP GET calls need to be made that are potentially merged together.

Getting Started with Service with a Subject in Angular

A powerful technique in Angular is the built in integration with RxJS. Making use of reactive programming (ie. RxJS) can remove a lot of code from your Angular application. This is an indictor that reactive programming takes care of a lot of the logic you would otherwise have to come up with and test yourself.

There are simple steps you can take to get started with writing reactive Angular applications. You can then use the understanding and confidence you get from the simple steps to start using reactive programming in other, more complicated cases.

Simple Service with a Subject

The following code is a simple Angular service with a subject.
name.service.ts

// name.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class NameService {
  private mName$ = new BehaviorSubject<string>(null);

  name$(): Observable<string> {
    return this.mName$;
  }

  constructor(private httpClient: HttpClient) { }

  loadName() {
    this.httpClient.get<string>('name URL')
      .subscribe(name => this.mName$.next(name));
  }
}

There are 3 pieces to the subject that come together to trigger getting the name from the API and passing on the retrieved name.

API Call

The entry point for a user of the service is the loadName function on line 18. It triggers the HTTP call to the API and passes on the retrieved name to mName$ in the subscribe call on line 20.

Storing the Retrieved Name

The mName$ member variable on line 10 of the service is used as private intermediate storage of the name retrieved from the API in the service. mName$ is tightly linked to the name$ function which is used to control access to the intermediate storage.

Passing on the Retrieved Name

The name$ function of the service is used to pass on the name retrieved from the API on line 12. Hiding the private intermediate storage mName$ behind the name$ function allows control over how the name is distributed throughout the Angular application.

The next step is to show the name in a component.

Reactive Component

Once the name has been retrieved from the API, the next step is to show it in a component.

Component Typescript Code

The following is the typescript code for the component.
name.component.ts

// name.component.ts
import { Component, OnInit } from '@angular/core';

import { NameService } from './name.service';

@Component({
  selector: 'app-name',
  templateUrl: './name.component.html',
  styleUrls: ['./name.component.css']
})
export class NameComponent implements OnInit {
  constructor(public nameService: NameService) { }

  ngOnInit() {
    this.nameService.loadName();
  }

}

There is hardly any code in the component that is not part of the required Angular component code. The only line that has been added is line 15 which instructs the service to load the name along with importing the service on line 4 and injecting it into the component on line 12.

Component Template

The following is the HTML template associated with the component.
name.component.html

<!-- name.component.html -->
<ng-container *ngIf="nameService.name$() | async as name">
  {{ name }}
</ng-container>

In the template the name$ function of the service is passed to the async pipe on line 2. The values emitted by the name$ observable are renamed to the name variable and then displayed on line 3.

That concludes the simple example for making use of a subject with a service. There are some additions to consider depending on the use case. For example, you may want to introduce error handling in the subject. You may also want to write tests for the service and the component.