【问题标题】:How to unit test unsubscribe function in angular如何以角度对取消订阅功能进行单元测试
【发布时间】:2019-02-25 17:19:41
【问题描述】:

我想找到一种方法来测试订阅和主题上的取消订阅函数调用。

我想出了一些可能的解决方案,但每一种都有利有弊。请记住,我不想为了测试目的而更改变量的访问修饰符。

  1. 通过反射访问组件的私有变量。

在这种情况下,我有一个存储订阅的私有类变量:

component.ts:

private mySubscription: Subscription;
//...
ngOnInit(): void {
    this.mySubscription = this.store
        .select(mySelector)
        .subscribe((value: any) => console.log(value));
}

ngOnDestroy(): void {
    this.mySubscription.unsubscribe();
}

component.spec.ts:

spyOn(component['mySubscription'], 'unsubscribe');
component.ngOnDestroy();
expect(component['mySubscription'].unsubscribe).toHaveBeenCalledTimes(1);

优点:

  • 我可以访问 mySubscription。
  • 我可以测试右边调用了unsubscribe方法 订阅。

缺点:

  • 我只能通过反射访问 mySubscription,如果可能,我希望避免这种情况。

  1. 我为订阅创建了一个变量,就像在选项 1 中一样。但是我没有通过反射来访问该变量,我只是检查了取消订阅方法是否被调用,而不知道源。

component.ts: 与选项 1 中的相同


component.spec.ts:

spyOn(Subscription.prototype, 'unsubscribe');
component.ngOnDestroy();
expect(Subscription.prototype.unsubscribe).toHaveBeenCalledTimes(1);

优点:

  • 我可以测试是否调用了取消订阅方法

缺点:

  • 我无法测试调用的取消订阅方法的来源。

  1. 我实现了一个辅助函数,它在传递的订阅参数上调用取消订阅方法。

subscription.helper.ts:

export class SubscriptionHelper {

    static unsubscribeAll(...subscriptions: Subscription[]) {
        subscriptions.forEach((subscription: Subscription) => {
                subscription.unsubscribe();
            });
    }
}

component.ts: 与选项 1 相同,但 ngOnDestroy 不同:

ngOnDestroy(): void {
    SubscriptionHelper.unsubscribeAll(this.mySubscription);
}

component.spec.ts:

spyOn(SubscriptionHelper, 'unsubscribeAll');
component.ngOnDestroy();
expect(SubscriptionHelper.unsubscribeAll).toHaveBeenCalledTimes(1);

优点:

  • 我可以测试是否调用了辅助函数

缺点:

  • 我无法测试是否在特定订阅上调用了取消订阅函数。

你们有什么建议?如何在单元测试中测试清理?

【问题讨论】:

  • 为什么要测试unsubscribe?这是 rxjs 的一部分,我认为您可以放心地假设它有效
  • 我认为 PO 只是想确保 unsubscribe 被调用。
  • 我想测试组件清理而不是取消订阅的行为。如果我想测试该行为,那么我将检查作为取消订阅调用来源的订阅是否不再处于活动状态。但同样,我想测试该组件是否在每次订阅时调用取消订阅。

标签: angular unit-testing rxjs observable subscription


【解决方案1】:

.component.ts:

...
private someSubscription!: Subscription;
...
ngOnInit(): void {
  this.someSubscription = ... .subscribe(() => ...);
}
...
ngOnDestroy(): void {
  this.someSubscription.unsubscribe();
}
...

.component.spec.ts:

...
it('should unsubscribe', () => {
  component['someSubscription'] = of().subscribe();
  const unsubscriptionSpy = jest.spyOn(component['someSubscription'], 'unsubscribe');
  component.ngOnDestroy();
  expect(unsubscriptionSpy).toHaveBeenCalled();
});
...

当修改ngOnDestroy(组件)内部的代码时,测试失败。因此,这个解决方案对我来说效果很好。

【讨论】:

    【解决方案2】:

    最务实的方法是把东西交到你手中,只使用语言功能。

    const cmp = fixture.componentInstance as any;
    cmp.mySubscription = jasmine.createSpyObj<Subscription>(['unsubscribe']);
    

    请记住,这绝对不是测试私有代码的模式。您仍然需要测试公共部分以间接证明私有代码。

    【讨论】:

      【解决方案3】:

      Lucien DulacParitosh 几乎是对的:

      component.ts:

      private subscriptions = new Subscription();
      
      constructor(): void {
          this.subscriptions.add(...);
      }
      
      ngOnDestroy(): void {
          this.subscriptions.unsubscribe();
      }
      

      component.spec.ts:

      spyOn(component['subscriptions'], 'unsubscribe');
      component.ngOnDestroy();
      expect(component['subscriptions'].unsubscribe).toHaveBeenCalled();
      

      【讨论】:

        【解决方案4】:

        我想出了一个办法让它发挥作用。

        首先,我不会为每个订阅创建一个订阅实例,我通常只使用一个订阅实例,并通过使用实现 teardownLogic 并允许子订阅的方法“add”添加到我需要的多个订阅中。

        private subscriptions: Subscription;
         //...
        ngOnInit(): void {
          this.mySubscription.add(
            this.store
              .select(mySelector)
              .subscribe((value: any) => console.log(value))
          );
        }
        
        ngOnDestroy(): void {
          this.subscriptions.unsubscribe();
        }
        

        通过这样做,在我的测试中,我可以依赖订阅原型并检测 取消订阅方法调用。在那种情况下,我调用了 ngOnDestroy。

        const spy = spyOn(Subscription.prototype, 'unsubscribe');
        component.ngOnDestroy();
        expect(spy).toHaveBeenCalledTimes(1);
        

        【讨论】:

          【解决方案5】:

          我遇到了完全相同的问题,这是我的解决方案:

          component.ts:

          private subscription: Subscription;
          //...
          ngOnInit(): void {
              this.subscription = this.route.paramMap.subscribe((paramMap: ParamMap) => {
                  // ...
              });
          }
          
          ngOnDestroy(): void {
              this.subscription.unsubscribe();
          }
          
          

          component.spec.ts:

          
          let dataMock;
          
          let storeMock: Store;
          let storeStub: {
              select: Function,
              dispatch: Function
          };
          
          let paramMapMock: ParamMap;
          let paramMapSubscription: Subscription;
          let paramMapObservable: Observable<ParamMap>;
          
          let activatedRouteMock: ActivatedRoute;
          let activatedRouteStub: {
              paramMap: Observable<ParamMap>;
          };
          
          beforeEach(async(() => {
              dataMock = { /* some test data */ };
          
              storeStub = {
                  select: (fn: Function) => of((id: string) => dataMock),
                  dispatch: jasmine.createSpy('dispatch')
              };
          
              paramMapMock = {
                  keys: [],
                  has: jasmine.createSpy('has'),
                  get: jasmine.createSpy('get'),
                  getAll: jasmine.createSpy('getAll')
              };
          
              paramMapSubscription = new Subscription();
              paramMapObservable = new Observable<ParamMap>();
          
              spyOn(paramMapSubscription, 'unsubscribe').and.callThrough();
              spyOn(paramMapObservable, 'subscribe').and.callFake((fn: Function): Subscription => {
                  fn(paramMapMock);
                  return paramMapSubscription;
              });
          
              activatedRouteStub = {
                  paramMap: paramMapObservable
              };
          
              TestBed.configureTestingModule({
                 // ...
                 providers: [
                     { provide: Store, useValue: storeStub },
                     { provide: ActivatedRoute, useValue: activatedRouteStub }
                 ]
              })
              .compileComponents();
          }));
          
          // ...
          
          it('unsubscribes when destoryed', () => {
              fixture.detectChanges();
          
              component.ngOnDestroy();
          
              expect(paramMapSubscription.unsubscribe).toHaveBeenCalled();
          });
          

          这对我有用,我希望它也对你有用!

          【讨论】:

          • 干净的评论,只是需要的方法和什么。
          【解决方案6】:

          你觉得这个好看吗?

          component.mySubscription = new Subscription();
          
          spyOn(component.mySubscription, 'unsubscribe');
          component.ngOnDestroy();
          
          expect(component.mySubscription.unsubscribe).toHaveBeenCalledTimes(1);
          

          【讨论】:

          • 在这种情况下,mySubscription 是公开的,但不应该公开。
          • 抱歉,我没看错。您问的内容有点令人困惑,因为您想测试根据定义假定为未知的事物的身份……将其保密有什么意义?它比公开更相关吗?
          【解决方案7】:

          我远离评论,以获得更多空间,即使我只是一个开放的推理。我希望这可以被 SO 接受。

          我想测试组件是否在每个 订阅

          我认为这是一个有趣的测试设计案例。

          我们都知道,当组件被销毁时,不要让订阅保持活动状态很重要,因此值得考虑如何测试这一点。

          同时,这可以被视为一个实现细节,它当然不属于 SW 组件通过其 API 公开的“可公开访问的行为” .如果您不想将测试和代码紧密耦合在一起,那么公共 API 应该是测试的重点领域,这会阻碍重构。

          另外,AFAIK RxJS 不提供在给定时刻列出所有活动订阅的机制,因此找到测试方法的任务仍然在我们身上。

          因此,如果您想达到测试所有订阅都取消订阅的目标,您需要采用特定的设计来实现这一点,例如使用存储所有订阅的数组,然后检查是否所有订阅都已关闭。这是您的SubscriptionHelper 的行。

          但即使这样也不一定能把你放在一个安全的地方。您始终可以创建一个新订阅而不将其添加到数组中,这可能只是因为您忘记在代码中执行此操作。

          因此,只有在您坚持编码规则(即向数组添加订阅)时,您才能对此进行测试。但这相当于确保您取消订阅 ngOnDestroy 方法中的所有订阅。即使这是编码纪律。

          那么,即使测试的最终目标很重要,进行这样的测试真的有意义吗?不应该通过代码审查来实现吗?

          很好奇阅读意见

          【讨论】:

          • 我也很好奇。请记住:如果我们创建一个数组来在其中存储订阅,我们仍然必须公开它,这不是一个好的做法,因为 API 的形状,或者我们必须通过反射来访问这个私有成员,我想避免的。
          • 明确一点:我认为这不应该通过测试用例进行测试,因为它将太多的实现细节与测试联系起来
          • 我认为应该对其进行测试,以确保内存没有泄漏。
          • 工作 TDD,在没有测试的情况下(几乎)不可能添加 unsubscribe 调用。这应该足够了。如果它不适合你,想想你的代码做了一些重要的事情,你想确保没有人停止做。根据定义,这是值得测试的。这不是关于耦合,而是关于编写可行的测试来验证单元的正确行为。关于测试它的机制,见我上面的回答。
          【解决方案8】:

          关于你的最后一点

          我无法测试是否在特定订阅上调用了取消订阅功能。

          其实你可以做到的。

          // get your subscrption
          const mySubscription = component['mySubscription'];
          
          // use the reference to confirm that it was called on that specific subscription only.
          expect(SubscriptionHelper.unsubscribeAll).toHaveBeenCalledWith(mySubscription);
          

          现在,当涉及到测试时,它应该只检查/访问/调用公共成员,并测试/预期其效果。

          使用 javascript 的奢华,你期待/验证私人成员,即使这很好 我认为。即使我这样做,我也可能是错的。欢迎富有成效的批评和 cmet。

          【讨论】:

          • 最后,我的两分钱:测试不应该驱动开发,但是,我们的代码应该是可测试的。
          • 我提出这个问题的意图正是为了澄清这种方法是否被广泛使用。或者也许有更好的方法来测试类似的东西。
          • 所以在我看来,你做得很好。我为您缺少 wrt 方法HaveBeenCalledWith 提供了答案。在访问私有变量时(当没有其他方法可以验证效果时),我会以同样的方式来确保代码经过测试。很好解释的问题。为此 +1!
          • “我只能通过反射访问我的订阅,如果可能的话我想避免这种情况。”您提出的解决方案包含我首先尝试避免的内容。我将等待其他评论者。也许他们还有其他我们还不知道的方法。
          猜你喜欢
          • 1970-01-01
          • 2017-02-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-03-09
          • 2019-09-24
          • 1970-01-01
          • 2021-10-20
          相关资源
          最近更新 更多