如何以角度对退订功能进行单元测试

时间:2018-09-21 10:05:55

标签: angular unit-testing rxjs observable subscription

我想找到一种方法来测试对“订阅和主题”的取消订阅功能的调用。

我想出了一些可能的解决方案,但是每个解决方案都有其优缺点。请记住,我不想出于测试目的而更改变量的访问修饰符。

  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,如果可能的话,我想避免这种事情。

  1. 我像选项1中一样为预订创建一个变量,但是没有反射地到达变量,我只是检查了unsubscribe方法是否被调用,而又不知道其来源。

component.ts:与选项1相同


component.spec.ts:

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

优点:

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

缺点:

  • 我无法测试调用的unsubscribe方法的来源。

  1. 我实现了一个辅助函数,该函数对传递的订阅参数调用unsubscribe方法。

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

优点:

  • 我可以测试助手函数是否被调用

缺点:

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

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

9 个答案:

答案 0 :(得分:2)

我想出一种使之工作的方法。

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

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

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

这样做,在我的测试中,我可以依靠Subscriptions原型并检测 取消订阅方法调用。在这种情况下,我打电话给ngOnDestroy。

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

答案 1 :(得分: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();
});

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

答案 2 :(得分:0)

关于最后一点

  

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

您可以实际做到。

// 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,您期望/验证私有成员,即使这样也很好,我认为。即使我这样做,我可能也是错的。欢迎进行生产性批评和评论。

答案 3 :(得分:0)

我放弃了COMMENTS,获得了更多空间,即使我只是一个开放的推理。我希望SO可以接受。

  

我想测试该组件是否在每个调用取消订阅   订阅

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

我们都知道,在组件被销毁时不要使订阅保持有效很重要,因此值得思考如何进行测试。

同时,这可以看作是实施细节,它当然不属于SW组件通过其API公开的“ 可公开访问的行为” 。如果您不希望将测试和代码紧密结合在一起,那么公共API应该成为测试的重点领域,这阻碍了重构。

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

因此,如果您要达到测试所有订阅都未订阅的目的,则需要采用特定的设计来允许这样做,例如使用存储所有订阅的数组,然后检查是否已关闭所有订阅。这是您的SubscriptionHelper所在的行。

但是即使这样也不一定能把您放在安全的地方。您总是可以创建一个新的订阅,而不将其添加到数组中,可能仅仅是因为您忘记在代码中这样做。

因此,只有遵守编码准则(即为数组添加订阅),您才能进行测试。但这等同于确保您使用ngOnDestroy方法取消订阅所有订阅。甚至这都是编码学科。

那么,即使测试的最终目标很重要,进行这样的测试真的有意义吗?通过代码审查可以达到目的吗?

很好奇阅读观点

答案 4 :(得分:0)

这对您来说好吗?

component.mySubscription = new Subscription();

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

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

答案 5 :(得分:0)

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

答案 6 :(得分:0)

最务实的做法是把事情掌握在自己手中,只是使用的语言功能。

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

请记住这是不absolutelly测试私有代码的模式。你仍然需要测试公共件间接地证明专用代码。

答案 7 :(得分:0)

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

答案 8 :(得分:-1)

对于我个人而言,我不介意使用第一个选项来选择私有mySubscription属性。

但是,如果您不想访问私有属性,并且在需要自己调用done函数的地方使用异步测试,则可以将其设为ngOnDestroy()的可选参数(这也期望您只能在组件生命周期内仅致电ngOnDestroy一次:

function ngOnDestroy(done?: () => void): void {
  this.mySubscription.add(done);
  this.mySubscription.unsubscribe();
}

现在调用ngOnDestroy会先取消订阅mySubscription,然后再调用done(如果已设置)。您知道订阅​​已取消订阅,因为测试通过并且没有超时。

然后在您的测试中,例如:

describe('', () => {
  it('', done => {
    const component = ...
    component.ngOnDestroy(done)
  })
})

未经测试的实时演示:https://stackblitz.com/edit/rxjs6-demo-rujesw?file=index.ts