Testing Angular Service with Observables

Testing Angular Service with Observables

Learn how to test a method that returns an Observable data

In this post, we will write a small service that returns an Observable of number, and then see how we can write a unit test to test the method.

To start, our service will be called DataService and it will contain a single method called getObservableData. For this post, there will be no dependency injection going on, and so there will not be any constructor either.

import {Injectable} from '@angular/core';
import {from, Observable} from 'rxjs';

@Injectable()
export class DataService {

  getObservableData(): Observable<number> {
    return from([1,2,3,4,5,6,7,8,9,10]);
  }
}

The getObservableData returns a stream of numbers and we use the from creation operator to generate this stream of data.

Here are the steps to test this service:

  1. Setup the TestBed by providing it a module definition. This module definition will contain all the things needed for the TestBed to give us an instance of our service.
  2. Get an instance of the service from the TestBed
  3. Test the getObservableData method using the instance provided by our TestBed

TestBed exposes several methods, and the one that we are going to use is the configureTestingModule method. It takes a module definition object, and the idea is that we define all the dependencies the service will need into this module. In other words, if the DataService service was using TranslationService, we would provide that service into this module definition. For this simple case, we only have to provide the DataService provider.

    TestBed.configureTestingModule({
      providers: [DataService]
    });

Once the TestBed has been configured with the module definition, an instance of the service can be requested as follows:

let dataService = TestBed.inject(DataService);

We can refactor our code so that the TestBed is configured fresh for every test spec that is run. This can be done with the beforeEach jasmine function. The beforeEach function takes an action callback, and we configure our TestBed module within it. An instance of the DataService is also obtained within this action callback.

   beforeEach(()=>{
    TestBed.configureTestingModule({
      providers: [DataService]
    });
    dataService = TestBed.inject(DataService);
  });

Notice that we are assigning the new instance of DataService to the dataService variable. This variable is declared in the global scope, hence, the .spec file should now look as follows:

import {DataService} from './data.service';
import {TestBed} from '@angular/core/testing';

describe('DataService', function () {
  let dataService: DataService;
  beforeEach(()=>{
    TestBed.configureTestingModule({
      providers: [DataService]
    });
    dataService = TestBed.inject(DataService);
  });
});

Testing our method that returns an Observable of number

To test the getObservable data method, we need to remind ourselves of a few things about observables, namely:

  1. Observables are triggered upon subscription. That means, to test the getObservable method, we have to subscribe to it.
  2. Observables can take time and the subscription block may run after the test has finished! That means, we need a way to tell the test framework to wait and not exit the it spec function until the subscription block has fired for all of our data.

How do you tell the test framework to wait for the observable block to finish before exiting the function?

The answer is quite simple. Use the done function (DoneFn). This function is passed to the assertion block, and it guarantees that the block will not exit until the done function is called.

Below is an example:

  it('should not finish until we call done', (done) => {
    // this block will not finish until we call done.
    done();
  });

There is one more problem to think about.

The observable returns a stream of numbers. That means, our block of code within the subscription will be executed multiple times. How can the expect statement be written such that we can test our array of numbers that will come to us?

Here is an example of what I mean:

  it('should return an observable data', (done) => {
    let expectedResult = [1,2,3,4,5,6,7,8,9,10];
    dataService.getObservableData()
      .subscribe(value => {
        // the following will fail - 
        //value will be 1 (and in future calls, 2, 3 ..etc)
        expect(value).toEqual(expectedResult);
        done();
      });
  });

We could create an array in our spec and then push values we get into that array. Using some if statements, the expect statement could be set to execute only when the length of the array matches our data.

However, there is a better approach! Use the rxjs toArray() operator.

Here is the final spec:

  it('should return an observable data', (done) => {
    let expectedResult = [1,2,3,4,5,6,7,8,9,10];
    dataService.getObservableData()
      .pipe(toArray())
      .subscribe(value => {
        expect(value).toEqual(expectedResult);
        done();
    });
  });

The rxjs toArray operator waits for the observable to complete, and then returns all the values as an array. That means, within our subscribe block, the value returned is an array. We can run the expectation and compare the value to our expectedResult variable (as shown above).

Now that the test has been created, everything should work. However, there are a few things I want to point out:

  1. Writing incorrect tests with Observables could give you false positives. For example, removing the done function parameter completely will pass the test. You will receive a warning however, and it will tell you that there were no expect statements in your code! In short, keep an eye on warning messages you get from running your tests.
  2. To make this examples simple, I created the getObservableData() function with a hard coded Observable data. In practice, the return would typically be from another service. As a best practice (I should say as a requirement), you should always mock dependencies. You don't want to make an actual call to the service - always mock the return data using spies.

In my next post, I will try to tackle the second problem above - how to mock dependencies.