web-dev-qa-db-fra.com

Comment tester la fonction de désabonnement unitaire dans angular

Je voudrais trouver un moyen de tester les appels de fonction de désabonnement sur les abonnements et les sujets.

J'ai trouvé quelques solutions possibles, mais chacune a ses avantages et ses inconvénients. Veuillez garder à l'esprit que je ne souhaite pas modifier le modificateur d'accès d'une variable à des fins de test.

  1. Accès à la variable privée du composant avec réflexion.

Dans ce cas, j'ai une variable de classe privée qui stocke un abonnement:

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

avantages:

  • Je peux accéder à mySubscription.
  • Je peux tester que la méthode de désabonnement a été invoquée sur le bon abonnement.

les inconvénients:

  • Je ne peux accéder à mon abonnement qu'avec réflexion, ce que j'aimerais éviter si possible.

  1. Je crée une variable pour l'abonnement comme dans l'option 1., mais au lieu d'atteindre la variable avec réflexion, je vérifie simplement que la méthode de désabonnement a été invoquée, sans connaître la source.

component.ts: identique à l'option 1


component.spec.ts:

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

avantages:

  • Je peux tester que la méthode de désabonnement a été appelée

les inconvénients:

  • Je ne peux pas tester la source de la méthode de désabonnement invoquée.

  1. J'ai implémenté une fonction d'assistance qui invoque la méthode unsubscribe sur les paramètres passés qui sont des abonnements.

souscription.helper.ts:

export class SubscriptionHelper {

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

component.ts: identique à l'option 1, mais ngOnDestroy est différent:

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

component.spec.ts:

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

avantages:

  • Je peux tester que la fonction d'aide a été appelée

les inconvénients:

  • Je ne peux pas tester que la fonction de désabonnement a été appelée sur un abonnement spécifique.

Que proposez-vous les gars? Comment testez-vous le nettoyage dans le test unitaire?

10
Balázs Takács

J'ai eu exactement le même problème, voici ma solution:

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

Cela fonctionne pour moi, j'espère que ça le sera aussi pour vous!

4
ethanfar

Cela vous semble-t-il bon?

component.mySubscription = new Subscription();

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

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

Je m'éloigne des COMMENTAIRES, pour gagner un peu plus d'espace, même si le mien n'est qu'un raisonnement ouvert. J'espère que c'est acceptable par SO.

Je voudrais tester que le composant appelle désabonnement à chaque abonnement

Je pense que c'est un cas intéressant de conception de test.

Nous savons tous qu'il est important de ne pas laisser les abonnements en vie lorsqu'un composant est détruit, et il vaut donc la peine de réfléchir à la manière de le tester.

En même temps, cela pourrait être considéré comme un détail d'implémentation et il n'appartient certainement pas au " accessible au public comportement "qu'un composant SW expose via ses API. Et les API publiques devraient être au centre des tests si vous ne souhaitez pas coupler étroitement les tests et le code, ce qui empêche le refactoring.

De plus, AFAIK RxJS ne fournit pas de mécanisme pour répertorier tous les abonnements actifs à un moment donné, donc la tâche de trouver un moyen de tester cela reste à nous.

Donc, si vous souhaitez atteindre votre objectif de tester que tous les abonnements ne sont pas souscrits, vous devez adopter une conception spécifique pour le permettre, par exemple l'utilisation d'un tableau où vous stockez tous vos abonnements, puis une vérification pour voir si tous ont été fermés. C'est la ligne de votre SubscriptionHelper.

Mais même cela ne vous met pas nécessairement en lieu sûr. Vous pouvez toujours créer un nouvel abonnement et ne pas l'ajouter au tableau, peut-être simplement parce que vous oubliez de le faire dans votre code.

Ainsi, vous ne pouvez tester cela que si vous vous en tenez à votre discipline de codage , c'est-à-dire que vous ajoutez des abonnements au tableau. Mais cela revient à vous assurer de vous désinscrire de tous vos abonnements dans la méthode ngOnDestroy. Même cela est une discipline de codage.

Alors, est-ce vraiment logique d'avoir un tel test même si l'objectif final du test est important? Cela ne devrait-il pas être quelque chose à réaliser via la révision du code?

Très curieux de lire les opinions

1
Picci

Concernant votre dernier point

Je ne peux pas tester que la fonction de désabonnement a été appelée sur un abonnement spécifique.

Vous pouvez le faire en fait.

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

Maintenant, en ce qui concerne les tests, il suffit de vérifier/accéder/appeler les membres publics et tester/attendre ses effets.

En utilisant le luxe de javascript, vous attendez/validez des membres privés, même si c'est très bien Je pense. Même si je le fais, je peux me tromper. Les critiques et commentaires productifs sont les bienvenus.

0
Paritosh