如何在 angular 中对取消订阅功能进行单元测试

How to unit test unsubscribe function in angular

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

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

  1. 正在使用反射访问组件的私有变量。

在那种情况下,我有一个私有 class 变量来存储订阅:

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

优点:

缺点:


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

优点:

缺点:


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

关于你的最后一点

I can not test that the unsubscribe function was called on a specific subscription.

你真的可以做到。

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

现在,当涉及到测试时,它应该只是 check/access/call public 个成员,以及 test/expect 个它的效果。

使用 javascript 的奢侈,您是 expecting/validating 私人会员,即使那样也很好 我认为。即使我那样做,我也可能是错的。欢迎富有成效的批评和评论。

我离开评论,以获得更多 space,即使我只是一个公开的推理。我希望这能被 SO 接受。

I would like to test that the component calls unsubscribe on every subscription

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

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

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

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

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

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

因此,只有在您坚持 编码规则 时才能测试它,即您向数组添加订阅。但这等同于确保您在 ngOnDestroy 方法中取消订阅所有订阅。即使这是编码纪律。

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

很好奇看意见

你觉得这个好看吗?

component.mySubscription = new Subscription();

spyOn(component.mySubscription, 'unsubscribe');
component.ngOnDestroy();

expect(component.mySubscription.unsubscribe).toHaveBeenCalledTimes(1);

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

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

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

我想出了一个让它工作的方法。

首先,我不会为每个订阅创建一个订阅实例,我通常只使用一个订阅实例,然后使用实现 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);

and 几乎答对了:

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

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

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

请记住,这绝对不是测试私有代码的模式。您还需要测试 public 件来间接证明私有代码。

.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(component)里面的代码,测试失败。因此,这个解决方案对我来说效果很好。