Testing RxJS Service with a Subject Component Interaction

The service with a subject pattern is a good entry point into the RxJS world of observables in Angular. When you are using RxJS, writing traditional tests can be cumbersome. There are great tools, such as jasmine-marbles, that help make these kinds of tests a lot easier. A great way to get started with jasmine-marbles is testing the service with a subject when the service only performs HTTP calls. However, when the service also accepts updates from components, writing these tests can be more difficult as you have to bridge the gap between the timing of function calls and observables.

The Service under Test

To illustrate a starting point for how to write these kinds of tests, consider a service that supports a component that allows the user to pick the sauce they would like to have on their burger 🍔. You might come up with a sauce service to keep track of which sauce was picked, such as the one shown below.
sauce.service.ts

// sauce.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SauceService {
  private mSauce$ = new BehaviorSubject<string>('initial sauce');

  constructor() {}

  sauce$(): Observable<string> {
    return this.mSauce$;
  }

  setSauce(sauce: string) {
    this.mSauce$.next(sauce);
  }
}

Testing the Service

There are two things you may be interested in testing. The first is that the initial sauce is set correctly and the second is that, on calling setSauce, that the new sauce is emitted by sauce$. Both of these can be tested using jasmine-marbles. Without jasmine-marbles, you would have to subscribe to sauce$, check that the correct sauces are emitted and make use of the done function. With jasmine-marbles this can be simplified.

To see how, you need to understand that the jasmine-marbles cold function simply creates an observable. Just like any other observable, you can subscribe to it. So if you define an observable that emits the sauces that you want to call setSauce with using cold, subscribe to that observable and call setSauce with the sauces that you receive, you have bridged the gap between function calls an observables. The marble syntax allows you to specify the timing of the setSauce calls. The tests could like like what is shown below.
sauce.service.spec.ts

// sauce.service.spec.ts
import { cold } from 'jasmine-marbles';

import { SauceService } from './sauce.service';


describe('SauceService', () => {
  let service: SauceService;

  beforeEach(() => {
    service = new SauceService();
  });

  describe('construction', () => {
    it('should emit initial sauce', () => {
      // Setting up expected observable
      const expectedMarbles = 'a';
      const expected = cold(expectedMarbles, {a: 'initial sauce'});

      // Checking sauce$
      expect(service.sauce$()).toBeObservable(expected);
    });
  });

  describe('setSauce', () => {
    it('should emit new sauce', () => {
      // Defining marbles
      const setSauceCallMarbles = '-a';
      const expectedMarbles =     'bc';

      // Setting up setSauce call
      cold(setSauceCallMarbles, {a: 'new sauce'})
        .subscribe(sauce => service.setSauce(sauce));

      // Checking sauce$
      const expected = cold(expectedMarbles, {b: 'initial sauce', c: 'new sauce'});
      expect(service.sauce$()).toBeObservable(expected);
    });
  });
});

The test on lines 15-22 checks that the initial sauce is emitted after construction. The interesting test is on lines 26-39. On line 28 the timing of the setSauce calls is defined. In this case, it is setup so that the first setSauce call occurs one frame after the construction is complete. On lines 32-33 the input to the setSauce call is defined and the plumbing to perform the setSauce calls is specified. On lines 36-37 the standard work is being done to check the values emitted by sauce$.

This technique is more concise and easy to understand than messing around with observables, subscriptions and the done function. There are still things that could be better. For example, it would be great if jasmine-marbles could come up with a standardised way of performing this technique (maybe they already have and I just don’t know about it). You may not feel like using this technique all the time, depending on the complexity of the logic and its dependence on timing. However, knowing about it and how it helps you can be valuable when you encounter the need for finer control over timing of function calls in tests.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s