如何在 angular 中对取消订阅功能进行单元测试
How to unit test unsubscribe function in angular
我想找到一种方法来测试对订阅和主题的取消订阅函数调用。
我提出了一些可能的解决方案,但每一个都各有利弊。请记住,我不想为了测试目的而更改变量的访问修饰符。
- 正在使用反射访问组件的私有变量。
在那种情况下,我有一个私有 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);
优点:
- 我可以访问我的订阅。
- 我可以测试右边调用了unsubscribe方法
订阅。
缺点:
- 我只能通过反射到达我的订阅,如果可能的话我想避免。
- 我像选项 1 一样为订阅创建了一个变量。但我没有通过反射访问变量,而是在不知道源的情况下检查是否调用了取消订阅方法。
component.ts: 同选项 1
component.spec.ts:
spyOn(Subscription.prototype, 'unsubscribe');
component.ngOnDestroy();
expect(Subscription.prototype.unsubscribe).toHaveBeenCalledTimes(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)里面的代码,测试失败。因此,这个解决方案对我来说效果很好。
我想找到一种方法来测试对订阅和主题的取消订阅函数调用。
我提出了一些可能的解决方案,但每一个都各有利弊。请记住,我不想为了测试目的而更改变量的访问修饰符。
- 正在使用反射访问组件的私有变量。
在那种情况下,我有一个私有 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);
优点:
- 我可以访问我的订阅。
- 我可以测试右边调用了unsubscribe方法 订阅。
缺点:
- 我只能通过反射到达我的订阅,如果可能的话我想避免。
- 我像选项 1 一样为订阅创建了一个变量。但我没有通过反射访问变量,而是在不知道源的情况下检查是否调用了取消订阅方法。
component.ts: 同选项 1
component.spec.ts:
spyOn(Subscription.prototype, 'unsubscribe');
component.ngOnDestroy();
expect(Subscription.prototype.unsubscribe).toHaveBeenCalledTimes(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);
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)里面的代码,测试失败。因此,这个解决方案对我来说效果很好。