2

I have written a unit test for this function:

getCarsAndSetup(){
    this.getCars();
    this.getFactoryInfo();
}

This is the getCars() function:

getCars() {
     const subscription = this.carDetailsService.getAll().subscribe((carDetails) => {
     this.carInfoService.setCars(carDetails);
     subscription.unsubscribe();  <-------------- Here the 
                                               subscription is undefined
                                                when running the test, 
                                                however when running
                                               the app, the subscription 
                                                is defined and
                                                  everything is fine
    });
}

This is the unit test:

fdescribe('getCarsAndSetup', () => {
    it('should get cars and set them up', () => {
        component.getFactoriesAndUnsubscribe();
        spyOn(component, "getCars");
        spyOn(component, "getFactoryInfo");
        expect(component.getCars).toHaveBeenCalled();
        expect(component.getFactoryInfo).toHaveBeenCalled();
    });
  });

I am using a mock for carDetailsService. This is the getAll() method in the carDetailsService mock:

getAll(): Observable<CarModel[]> {
    return Observable.create((observer:any) => {
        observer.next([]);
    });
}

And this is the same method in the REAL carDetailsService:

getAll(): Observable<CarModel[]> {
    return this.http.get<CarModel[]>(this.carUrl);
}

The problem is that when I run the application itself, the subscription in the getCars() method is defined, I can unsubscribe from it etc. and everything is fine.

However when I run the tests, this test fails, because for some reason the subscription is undefined in the getCars() function when I try to unsubscribe from it.

What could be the reason that the subscription is undefined only when running the test? Could it have something to do with the way I've mocked the getAll() function of carDetailsService?

Ivan Kaloyanov
  • 1,748
  • 6
  • 18
  • 24
GeForce RTX 4090
  • 3,091
  • 11
  • 32
  • 58
  • 1
    Could you use the pipeable operator take(1) ? As you unsubscribe just after receiving the data, take(1) will unsubscribe the Observable. Also, with your piece of code, are you sure the mocked observable is not throwing an error? If you add the error parameter in subscribe, do you get anything? I am wondering if you don't unsubscribe when there is an error. – Pierre R-A Jan 09 '19 at 11:40

2 Answers2

1

The problem here is that you rely on synchronous/asynchronous behaviour of your source Observable.

In your real app your this.carDetailsService.getAll() is a real remote call (asynchronous) so its subscription is assigned to subscription and everything works. In your tests however the same call is probably mocked and therefore synchronous so by the time you want to call subscription.unsubscribe() it's still undefined (the subscribe method is still executing and no subscription has been returned yet).

The easiest thing you can do is instead passing an arrow function to subscribe use function keyword. RxJS binds this inside subscriber handlers to its internal Subscription object (I know it's a bit tricky approach but it's intended to be used this way).

const that = this;
this.carDetailsService.getAll().subscribe(function(carDetails) { // note the `function` keyword
  that.carInfoService.setCars(carDetails);
  this.unsubscribe();
});

Another method could be using takeUntil with a Subject and completing it inside your subscribe.

This behavior might change in the future: https://github.com/ReactiveX/rxjs/issues/3983

The same problem in a different use-case: RxJs: Calculating observable array length in component

martin
  • 93,354
  • 25
  • 191
  • 226
  • Thanks, this did get rid of the error, and lead me to the next error which showed that I just had defined the spies in the wrong place. If I define the spies ABOVE the function call, I don't get any errors even without the changes you've described in your answer. – GeForce RTX 4090 Jan 09 '19 at 12:05
  • Marking this as the correct answer as I now encountered multiple similar situations in my next unit tests where this was the only solution. – GeForce RTX 4090 Jan 09 '19 at 13:26
0

While martin's answer did get rid of the error, it helped me discover the actual problem here, which is ridiculously silly. I had set up the spies AFTER the actual function call:

fdescribe('getCarsAndSetup', () => {
    it('should get cars and set them up', () => {
        component.getFactoriesAndUnsubscribe();
        spyOn(component, "getCars");
        spyOn(component, "getFactoryInfo");
        expect(component.getCars).toHaveBeenCalled();
        expect(component.getFactoryInfo).toHaveBeenCalled();
    });
  });

When the spies had to be defined BEFORE the actual function call:

fdescribe('getCarsAndSetup', () => {
    it('should get cars and set them up', () => {
        spyOn(component, "getCars");
        spyOn(component, "getFactoryInfo");
        component.getFactoriesAndUnsubscribe();
        expect(component.getCars).toHaveBeenCalled();
        expect(component.getFactoryInfo).toHaveBeenCalled();
    });
  });

I feel bad that martin spent so much time on this answer and reading the long description I posted, and it turns out the whole problem was just a small oversight. But it is what it is.

GeForce RTX 4090
  • 3,091
  • 11
  • 32
  • 58