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.

Testing Decorated Python Functions

Decorators are a great way of adding functionality to a function with minimal impact on the function itself. On top of that, decorator logic can be re-used on other functions that also require the new functionality. For example the following decorator prints a message to standard output every time a function is called.

# plain_main.py
"""Demonstrates a simple decorator."""


def decorator(func):
    """
    A simple decorator that adds printing a message on a function call.

    Args:
        func: The function to decorate.

    Returns:
        The decorated function.
    """
    def inner(*args, **kwargs):
        """Function that is called instead of original function."""
        print('The decorator was called.')
        return func(*args, **kwargs)

    return inner


@decorator
def main():
    print('The main function was called.')


if __name__ == '__main__':
    print('Calling the main function.')
    main()
$ python3 plain_main.py 
Calling the main function.
The decorator was called.
The main function was called.

The drawback of decorators is that the decorator is applied as soon as the interpreter reaches the function definition and it is hard to access the original function without the decorator applied. This might be desirable during testing where testing of the function and the decorator should be separated.

Adding Testing Guard Logic to a Decorator

The solution is to optionally skip the decorator logic if a certain condition is met that is only true during testing. For example, skip the decorator logic if the TESTING environment variable is set.

# check_main.py
"""Demonstrates a decorator with a testing guard."""

import os


def decorator(func):
    """
    A simple decorator that adds printing a message on a function call unless
    the TESTING environment variable is set.

    Args:
        func: The function to decorate.

    Returns:
        The decorated function.
    """
    def inner(*args, **kwargs):
        """Function that is called instead of original function."""
        # Checking for TESTING environment variable
        if os.getenv('TESTING') is not None:
            # Skipping decortor logic
            return func(*args, **kwargs)

        # Running decorator logic
        print('The decorator was called.')
        return func(*args, **kwargs)

    return inner


@decorator
def main():
    print('The main function was called.')


if __name__ == '__main__':
    print('Calling the main function without TESTING set.')
    main()

    print('Calling the main function with TESTING set.')
    os.environ['TESTING'] = ''
    main()
$ python3 check_main.py 
Calling the main function without TESTING set.
The decorator was called.
The main function was called.
Calling the main function with TESTING set.
The main function was called.

As you can see, the decorator logic was executed under normal circumstances (main call on line 39) and was skipped when the TESTING environment variable was set (main call on line 43). The reason was because of the guard statement on line 21 that checks for the TESTING environment variable.

Guard Decorator

You might now say: “great, thank you David. Now I have to rewrite all of my decorator functions!” Ah, but you don’t. If you stay with me through a little more complex decorator code, you won’t have to! The idea is to write a decorator that modifies another decorator’s behaviour.

# guard_main.py
"""Demonstrates a guard decorator."""

import os


def testing_guard(decorator_func):
    """
    Decorator that only applies another decorator if the TESTING environment
    variable is not set.

    Args:
        decorator_func: The decorator function.

    Returns:
        Function that calls a function after applying the decorator if TESTING
        environment variable is not set and calls the plain function if it is set.
    """
    def replacement(original_func):
        """Function that is called instead of original function."""
        def apply_guard(*args, **kwargs):
            """Decides whether to use decorator on function call."""
            if os.getenv('TESTING') is not None:
                return original_func(*args, **kwargs)
            return decorator_func(original_func)(*args, **kwargs)

        return apply_guard
    return replacement


@testing_guard
def decorator(func):
    """
    A simple decorator that adds printing a message on a function call.

    Args:
        func: The function to decorate.

    Returns:
        The decorated function.
    """
    def replacement(*args, **kwargs):
        """Function that is called instead of original function."""
        print('The decorator was called.')
        return func(*args, **kwargs)

    return replacement


@decorator
def main():
    print('The main function was called.')


if __name__ == '__main__':
    print('Calling the main function without TESTING set.')
    main()

    print('Calling the main function with TESTING set.')
    os.environ['TESTING'] = ''
    main()
$ python3 guard_main.py 
Calling the main function without TESTING set.
The decorator was called.
The main function was called.
Calling the main function with TESTING set.
The main function was called.

As you can see, the behaviour of the code is exactly the same but the decorator is in its original form. The reason this works is because the guard decorator gets to intercept each function call and can then decide whether to first apply the decorator or call the plain function on lines 23-25.

On top of not having to re-write decorator functions which you don’t want to execute during testing, you also get to separate the logic that determines whether the decorator is applied from the decorator logic which will reduce the chances of accidentally executing decorator logic as you make changes to the decorator. It also helps you write clear unit tests for both the decorator and the guard decorator.

Working with Pytest

The last consideration is how do you apply this in practice. I would argue that, unless you are testing the decorator or guard decorator, you should always have the TESTING environment variable set. This ensures that you are only testing function logic and not decorator logic. You can achieve this by putting a fixture in your root conftest.py file with autouse set to True.

@pytest.fixture(scope='function', autouse=True)
def set_testing(monkeypatch):
    """Sets the TESTING environment variable."""
    monkeypatch.setenv('TESTING', '')

When you are testing the decorator you would always ensure that the TESTING environment variable is not set. You can achieve that using a fixture that clears the TESTING environment variable that also has autouse set to True as a part of the file that tests the decorator.

@pytest.fixture(scope='function', autouse=True)
def delete_testing(monkeypatch):
    """Deletes the TESTING environment variable."""
    monkeypatch.delenv('TESTING', raising=False)

Finally, for testing the guard decorator, make whether the TESTING environment variable is set part of the tests themselves. Don’t be shy about overriding the functionality of any autouse fixtures as a part of the test function to demonstrate what the intended state of the test is clearly.

def test_guard_decorator_testing_set(monkeypatch):
    """
    GIVEN TESTING environment variable set and ...
    WHEN ...
    THEN ...
    """
    # Setting TESTING environment variable
    monkeypatch.setenv('TESTING', '')

    # Other test code


def test_guard_decorator_testing_not_set(monkeypatch):
    """
    GIVEN TESTING environment variable is not set and ...
    WHEN ...
    THEN ...
    """
    # Setting TESTING environment variable
    monkeypatch.delenv('TESTING', raising=False)

    # Other test code

Thats it! Consider whether environment variables is the best way of indicating that decorator logic should be skipped during testing and also what the best name of the environment variable is for you. I hope this was useful to you!