【问题标题】:Subscription to an observable is undefined only when running Angular Jasmine test, but is defined when running the app itself仅在运行 Angular Jasmine 测试时未定义对 observable 的订阅,但在运行应用程序本身时定义
【发布时间】:2019-06-04 03:50:31
【问题描述】:

我已经为这个函数写了一个单元测试:

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

这是 getCars() 函数:

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

这是单元测试:

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

我正在使用 carDetailsS​​ervice 的模拟。这是 carDetailsS​​ervice 模拟中的 getAll() 方法:

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

这与 REAL carDetailsS​​ervice 中的方法相同:

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

问题是当我运行应用程序本身时,在 getCars() 方法中定义了订阅,我可以取消订阅等等,一切都很好。

但是,当我运行测试时,此测试失败,因为由于某种原因,当我尝试取消订阅时,getCars() 函数中的订阅未定义。

仅在运行测试时未定义订阅的原因可能是什么?这可能与我嘲笑 carDetailsS​​ervice 的 getAll() 函数的方式有关吗?

【问题讨论】:

  • 你可以使用管道运算符 take(1) 吗?当您在收到数据后取消订阅时,take(1) 将取消订阅 Observable。另外,使用您的代码,您确定模拟的 observable 没有抛出错误吗?如果在订阅中添加错误参数,你会得到什么吗?我想知道您是否在出现错误时不取消订阅。

标签: angular unit-testing jasmine rxjs karma-jasmine


【解决方案1】:

这里的问题是您依赖源 Observable 的同步/异步行为。

在您的真实应用中,您的this.carDetailsService.getAll() 是一个真正的远程调用(异步),因此它的订阅分配给subscription,一切正常。但是,在您的测试中,可能会模拟相同的调用,因此是同步的,因此当您想调用 subscription.unsubscribe() 时,它仍然是 undefinedsubscribe 方法仍在执行,尚未返回订阅)。

您可以做的最简单的事情是将箭头函数传递给subscribe 使用function 关键字。 RxJS 将订阅者处理程序内部的this 绑定到其内部订阅对象(我知道这有点棘手,但它打算以这种方式使用)。

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

另一种方法是使用带有主题的takeUntil 并在您的subscribe 中完成它。

这种行为将来可能会改变:https://github.com/ReactiveX/rxjs/issues/3983

不同用例中的相同问题:RxJs: Calculating observable array length in component

【讨论】:

  • 谢谢,这确实消除了错误,并引导我进入下一个错误,这表明我刚刚在错误的地方定义了间谍。如果我在函数调用上方定义间谍,即使没有您在答案中描述的更改,我也不会收到任何错误。
  • 将此标记为正确答案,因为我现在在下一个单元测试中遇到了多个类似情况,这是唯一的解决方案。
【解决方案2】:

虽然马丁的回答确实消除了错误,但它帮助我发现了这里的实际问题,这非常愚蠢。我在实际的函数调用之后设置了间谍:

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

当必须在实际函数调用之前定义间谍时:

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

马丁在这个答案上花费了这么多时间并阅读了我发布的长篇描述,我感到很遗憾,事实证明整个问题只是一个小小的疏忽。但事实就是如此。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-08-15
    • 1970-01-01
    • 2023-03-22
    • 2017-07-20
    • 1970-01-01
    • 1970-01-01
    • 2017-06-07
    • 2017-03-01
    相关资源
    最近更新 更多