web-dev-qa-db-ja.com

angularでサブスクライブ解除機能をユニットテストする方法

サブスクリプションとサブジェクトのサブスクライブ解除関数呼び出しをテストする方法を見つけたいのですが。

私はいくつかの可能な解決策を思いつきましたが、これらのすべてには長所と短所があります。テストのために変数のアクセス修飾子を変更したくないことに注意してください。

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

長所:

  • Unsubscribeメソッドが呼び出されたことをテストできます

短所:

  • 呼び出された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);

長所:

  • ヘルパー関数が呼び出されたことをテストできます

短所:

  • Unsubscribe関数が特定のサブスクリプションで呼び出されたことをテストできません。

君たちは何を示唆しているの?単体テストでクリーンアップをどのようにテストしますか?

10
Balázs Takács

私はまったく同じ問題を抱えていました、これが私の解決策です:

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

これは私にとってはうまくいきます、あなたにもそれがうまくいくことを願っています!

4
ethanfar

よろしいですか?

component.mySubscription = new Subscription();

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

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

私がオープンな推論であったとしても、私はコメントから離れて、より多くのスペースを獲得します。これがSOによって受け入れられることを願っています。

コンポーネントがすべてのサブスクリプションで登録解除を呼び出すことをテストしたい

これはテスト設計の興味深いケースだと思います。

コンポーネントが破棄されたときにサブスクリプションを存続させないことが重要であることは誰もが知っているので、これをテストする方法を検討する価値があります。

同時に、これは実装の詳細と見なすことができ、「公的にアクセス可能なものに属していません。 SWコンポーネントがそのAPIを介して公開するBehavior "また、テストとコードを密に結合したくない場合は、パブリックAPIをテストの重点領域にする必要があります。これは、リファクタリングを妨げるものです。

さらに、AFAIK RxJSは、特定の瞬間にすべてのアクティブなサブスクリプションを一覧表示するメカニズムを提供しないため、これをテストする方法を見つける作業は私たちに残っています。

したがって、すべてのサブスクリプションがサブスクライブされていないことをテストするという目標に到達したい場合は、これを可能にする特定の設計を採用する必要があります。すべてのサブスクリプションを格納する配列を使用し、すべてのサブスクリプションが閉じているかどうかを確認します。これはSubscriptionHelperの行です。

しかし、これでも必ずしも安全な場所にいるわけではありません。常に新しいサブスクリプションを作成し、それを配列に追加することはできません。これは、コードでこれを行うのを忘れたためかもしれません。

したがって、これをテストできるのは、コーディング規律に固執する場合、つまり、サブスクリプションを配列に追加する場合のみです。ただし、これは、ngOnDestroyメソッドですべてのサブスクリプションを確実にサブスクライブ解除することと同じです。これもコーディングの規律です。

では、テストの最終目的が重要であっても、そのようなテストを行うことは本当に意味がありますか?コードレビューで達成すべきものではないでしょうか?

非常に好奇心が強い意見を読む

1
Picci

最後のポイントについて

Unsubscribe関数が特定のサブスクリプションで呼び出されたことをテストできません。

あなたはそれを実際に行うことができます。

// 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を使用して、プライベートメンバーを期待/検証していますが、それでも問題ありません。それでも私は間違っているかもしれません。生産的な批判とコメントを歓迎します。

0
Paritosh