79

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.

Erdinc Guzel
  • 937
  • 1
  • 6
  • 10
  • The other problem is when getCars doesn't emit any values(for whatever reason) test passes(since there are no expect checks!) – Wildhammer Jul 03 '19 at 20:01

6 Answers6

90

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

JeB
  • 11,653
  • 10
  • 58
  • 87
  • 3
    So, I found that if I do this, then specify another test later, depending on the order the tests run in the subscription from the other test is still active. What is the right way to address that? Unsubscribe at the end of the lambda function? – theMayer May 31 '18 at 15:04
  • 1
    Note that async in jasmine is supported starting from 2.7.0 – Pieter De Bie Jul 12 '18 at 11:45
  • 1
    @PieterDeBie you're mixing `async` keyword (that is indeed supported starting from Jasmine 2.7.0) and `async` function from Angular testing utilities (`import { async } from '@angular/core/testing';`). In this particular example it is the latter. – JeB Jul 13 '18 at 10:06
  • That's weird, I use `it('', async () => {...})` without importing anything. – Pieter De Bie Jul 13 '18 at 10:49
  • 1
    As I said, you're talking about `async/await`: https://jasmine.github.io/tutorials/async. This is Javascript feature, not Jasmine: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function. The only reason it is supported in Jasmine 2.7 is because Promises are supported in 2.7. Here we're talking about different case - Observables, and Angular has a special function called `async` which allows you testing asynchronous Observables. – JeB Jul 14 '18 at 07:24
  • 1
    async as a keyword seemed to had fixed my particular case, but you are completely right. – Pieter De Bie Jul 19 '18 at 07:48
  • What's the proper way to unsubscribe from the observable in the 'it' block? I'm seeing many tests in our codebase being connected and running the subscribe block inside other tests since it's not being unsubscribed. Just looking for the recommended clean up pattern.... – Greg Van Gorp Jul 19 '21 at 15:40
  • `The correct way` - really? What about case when `getCars()` never emit? Your test will green but actually it fail. – Jan 'splite' K. Jan 29 '22 at 17:30
  • @Jan'splite'K. I love the expression - great way to let everyone know how you feel and how much you disagree. In this particular question OP refers to an HTTP call made from within `CarService`. As I mentioned in the answer, `waitForAsync` will run all the async calls inside a special Zone and in case the promise is never resolved (i.e. observable never emits) the test will fail on timeout. In this case it is equivalent to `(done) => o.subscribe(() => done();)`. – JeB Jan 30 '22 at 14:02
  • @JeB or maybe english is not my first language and something get lost in translation :) - anyway, thanks for clarification. – Jan 'splite' K. Jan 30 '22 at 14:28
21

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();
    });
});
Marcus
  • 3,459
  • 1
  • 26
  • 25
13

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.

Erdinc Guzel
  • 937
  • 1
  • 6
  • 10
  • I think its because the `Promise` will not resolve if the `Observable` sequence is empty at the moment you call `toPromise()`. https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/topromise.md#returns – philipooo Jan 29 '16 at 16:23
  • I am not much experienced with this Promise and Observable stuff but if I log something to console, I see that the promise resolves and logs to console at some point. – Erdinc Guzel Feb 05 '16 at 09:58
8

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

SeanCheey
  • 143
  • 1
  • 4
1

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;      
}) );
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • I had seen the examples of `AsyncTestCompleter` in angular repo but it seems like `AsyncTestCompleter` is internal to angular and not found in public API. – Erdinc Guzel Jan 06 '16 at 07:28
  • I updated my answer. I haven't tried this because I don't use TS myself. – Günter Zöchbauer Jan 06 '16 at 14:32
  • The updated answer does not work unfortunately. inject method does not allow to return Promise values. By the way the injectAsync is again undeprecated with [#5721](https://github.com/angular/angular/pull/5721) – Erdinc Guzel Jan 07 '16 at 09:28
1

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();
 });
});

Update: Another way of doing this that I've found useful is to convert the observable into a promise if you know you only need one value. You can convert using either lastValueFrom or firstValueFrom. Then use a regular async/await structure for the test.

it('should equal bar', async () => {
 const result = await firstValueFrom(bar.getFoo())
 expect(result).toBe('foo')
});

It leads to a slightly shorter expect and setup so this was something we preferred at my old project.

Oldalf
  • 31
  • 7