简体   繁体   中英

Subscription to an observable is undefined only when running Angular Jasmine test, but is defined when running the app itself

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?

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM