web-dev-qa-db-fra.com

Test de cas d'erreur avec des observables dans des services

Disons que j'ai un composant qui s'abonne à une fonction de service:

export class Component {

   ...

    ngOnInit() {
        this.service.doStuff().subscribe(
            (data: IData) => {
              doThings(data);
            },
            (error: Error) => console.error(error)
        );
    };
};

L'appel d'abonnement prend deux fonctions anonymes en tant que paramètres. J'ai réussi à configurer un test d'unité de travail pour la fonction de données, mais Karma n'accepte pas la couverture pour l'erreur une.

enter image description here

J'ai essayé d'espionner la fonction console.error, en générant une erreur, puis en m'attendant à ce que l'espion soit appelé, mais cela ne suffit pas.

Mon test unitaire:

spyOn(console,'error').and.callThrough();

serviceStub = {
        doStuff: jasmine.createSpy('doStuff').and.returnValue(Observable.of(data)),
    };

    serviceStub.doStuff.and.returnValue(Observable.throw(

        'error!'
    ));

serviceStub.doStuff().subscribe(

    (res) => {

        *working test, can access res*
    },
    (error) => {

      console.error(error);
      console.log(error);  //Prints 'error!' so throw works.
      expect(console.error).toHaveBeenCalledWith('error!'); //Is true but won't be accepted for coverage.
    }
);

Quelle est la meilleure pratique pour tester des fonctions anonymes telles que celles-ci? Quel est le strict minimum pour garantir la couverture du test?

20
user3656550

Vous pouvez simplement simuler un objet d'erreur d'observation observable comme Observable.throw({status: 404}) et tester un bloc d'erreur d'observable.

const xService = fixture.debugElement.injector.get(SomeService);
const mockCall = spyOn(xService, 'method')
                       .and.returnValue(Observable.throw({status: 404}));

Mise à jour 2019:

Puisque certaines personnes sont paresseuses à lire les commentaires, laissez-moi mettre ceci ici: C'est une bonne pratique d'utiliser des erreurs pour Rxjs

import { throwError } from 'rxjs'
const xService = fixture.debugElement.injector.get(SomeService);
const mockCall = spyOn(xService,'method').and.returnValue(throwError({status: 404}));
27
Aniruddha Das

Vous ne savez pas exactement l'objectif du code que vous affichez, qui consiste à tester un service factice. Le problème de couverture concerne le composant et le rappel d'erreur n'a pas été appelé (ce dernier n'est appelé qu'en cas d'erreur).

Ce que je fais habituellement pour la plupart de mes services observables, est de créer un simulacre dont les méthodes reviennent tout seul. Le service fictif a une méthode subscribe qui accepte les rappels next, error et complete. L'utilisateur de la maquette doit le configurer pour ajouter une erreur afin que la fonction error soit appelée, ou pour ajouter des données, afin que la méthode next soit appelée. La chose que j'aime le plus à ce propos est que tout est synchrone.

Ci-dessous est quelque chose comme ce que j'utilise normalement. C'est juste une classe abstraite que d'autres simulacres peuvent étendre. Il fournit les fonctionnalités de base fournies par un observable. Le service fictif d'extension devrait juste ajouter les méthodes dont il a besoin, en se retournant dans la méthode.

import { Subscription } from 'rxjs/Subscription';

export abstract class AbstractMockObservableService {
  protected _subscription: Subscription;
  protected _fakeContent: any;
  protected _fakeError: any;

  set error(err) {
    this._fakeError = err;
  }

  set content(data) {
    this._fakeContent = data;
  }

  get subscription(): Subscription {
    return this._subscription;
  }

  subscribe(next: Function, error?: Function, complete?: Function): Subscription {
    this._subscription = new Subscription();
    spyOn(this._subscription, 'unsubscribe');

    if (next && this._fakeContent && !this._fakeError) {
      next(this._fakeContent);
    }
    if (error && this._fakeError) {
      error(this._fakeError);
    }
    if (complete) {
      complete();
    }
    return this._subscription;
  }
}

Maintenant, dans vos tests, vous faites juste quelque chose comme

class MockService extends AbstractMockObservableService {
  doStuff() {
    return this;
  }
}

let mockService;
beforeEach(() => {
  mockService = new MockService();
  TestBed.configureTestingModule({
    providers: [{provide: SomeService, useValue: mockService }],
    declarations: [ TestComponent ]
  });
});
it('should call service success', () => {
  mockService.content = 'some content';
  let fixture = TestBed.createComponent(TestComponent);
  // test component for success case
});
it('should call service error', () => {
  mockService.error = 'Some error';
  let fixture = TestBed.createComponent(TestComponent);
  // test component for error case
  // this should handle your coverage problem
});

// this assumes you have unsubscribed from the subscription in your
// component, which you should always do in the ngOnDestroy of the component
it('should unsubscribe when component destroyed', () => {
  let fixture = TestBed.createComponent(TestComponent);
  fixture.detectChanges();
  fixture.destroy();
  expect(mockService.subscription.unsubscribe).toHaveBeenCalled();
})
16
Paul Samsotha