Unit testing an observable in Angular 2

Unit TestingJasmineAngularObservable

Unit Testing Problem Overview


What is the correct way of unit testing a service returning an Observable result in Angular 2? Let's say we have a getCars method in a CarService service class:

...
export class CarService{
    ...
    getCars():Observable<any>{
        return this.http.get("http://someurl/cars").map( res => res.json() );
    }
    ...
}

If I try to write the tests in the following way I get the warning: 'SPEC HAS NO EXPECTATIONS':

it('retrieves all the cars', inject( [CarService], ( carService ) => {
     carService.getCars().subscribe( result => {         
         expect(result.length).toBeGreaterThan(0);
     } );       
}) );

Using injectAsync does not help because it works with Promise objects as far as I could see.

Unit Testing Solutions


Solution 1 - Unit Testing

The correct way for Angular (ver. 2+):

it('retrieves all the cars', waitForAsync(inject([CarService], (carService) => {
     carService.getCars().subscribe(result => expect(result.length).toBeGreaterThan(0)); 
}));

Async Observables vs Sync Observables

It is important to understand that Observables can be either synchronous or asynchronous.

In your specific example the Observable is asynchronous (it wraps an http call).
Therefore you have to use waitForAsync function that executes the code inside its body in a special async test zone. It intercepts and keeps track of all promises created in its body, making it possible to expect test results upon completion of an asynchronous action.

However, if your Observable was a synchronous one, for example:

...
export class CarService{
    ...
    getCars():Observable<any>{
        return Observable.of(['car1', 'car2']);
    }
    ...

you wouldn't have needed waitForAsync function and your test would become simply

it('retrieves all the cars', inject([CarService], (carService) => {
     carService.getCars().subscribe(result => expect(result.length).toBeGreaterThan(0)); 
});

Marbles

Another thing to consider when testing Observables in general and Angular in particular is marble testing.

Your example is pretty simple, but usually the logic is more complex than just calling http service and testing this logic becomes a headache.
Marbles make the test very short, simple and comprehensive (it is especially useful for testing ngrx effects).

If you're using Jasmine you can use jasmine-marbles, for Jest there is jest-marbles, but if you prefer something else, there is rxjs-marbles, that should be compatible with any test framework.

Here is a great example for reproducing and fixing a race condition with marbles.


Official guide for testing

Solution 2 - Unit Testing

https://angular.io/guide/testing currently shows a few ways. Here is one:

> it('#getObservableValue should return value from observable', > (done: DoneFn) => { > service.getObservableValue().subscribe(value => { > expect(value).toBe('observable value'); > done(); > }); > });

Solution 3 - Unit Testing

Finally I end with a working example. Observable class has a method toPromise that converts an Observable to a Promise object. The correct way should be:

it('retrieves all the cars', injectAsync( [CarService], ( carService ) => {
  return carService.getCars().toPromise().then( (result) => {         
     expect(result.length).toBeGreaterThan(0);
  } );       
}) );

But while to above code works with any Observable object, I still have the problem with the Observables returned from Http requests which is probably a bug. Here is a plunker demonstrating the case above: http://plnkr.co/edit/ak2qZH685QzTN6RoK71H?p=preview

Update:
As of version beta.14 it seems to work properly with the provided solution.

Solution 4 - Unit Testing

I recommend this approach, which I think is more elegant:

expectAsync(carService.getCars().toPromise()).toBeResolvedWith(myExpectedValue);

You can also provide your own async matchers using: Jasmine Matcher

Solution 5 - Unit Testing

AsyncTestCompleter is deprecated https://github.com/angular/angular/issues/5443. injectAsync replaced it https://github.com/angular/angular/issues/4715#issuecomment-149288405
but injectAsync is now also deprecated
injectAsync is not deprecated anymore https://github.com/angular/angular/pull/5721 (see also comment from @ErdincGuzel)

it('retrieves all the cars', injectAsync( [CarService], ( carService ) => {
     var c = PromiseWrapper.completer();
     carService.getCars().subscribe( result => {         
         expect(result.length).toBeGreaterThan(0);
         c.resolve();
     } ); 
     return c.promise;      
}) );

Solution 6 - Unit Testing

The way I've managed to get it to work is to subscribe and call done after the expects.

it('should equal bar', (done: any) => {
 bar.getFoo().subscribe(v => {
  expect(v).toBe('foo');
  done();
 });
});

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionErdinc GuzelView Question on Stackoverflow
Solution 1 - Unit TestingJeBView Answer on Stackoverflow
Solution 2 - Unit TestingMarcusView Answer on Stackoverflow
Solution 3 - Unit TestingErdinc GuzelView Answer on Stackoverflow
Solution 4 - Unit TestingSeanCheeyView Answer on Stackoverflow
Solution 5 - Unit TestingGünter ZöchbauerView Answer on Stackoverflow
Solution 6 - Unit TestingOldalfView Answer on Stackoverflow