angular - Testarea cazului de eroare cu observabile în servicii


Să zicem că am un component care se abonează la o funcție de serviciu:

export class Component {

   ...

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

Apelul subscribe ia două funcții anonime ca parametri, am reușit să configurez un test de unitate funcțional pentru funcția de date, dar Karma nu va accepta acoperirea pentru cea de eroare.

Am încercat să spionez funcția console.error, să arunc o eroare și apoi să mă aștept ca spionul să fi fost apelat, dar asta nu funcționează chiar așa.

Testul meu de unitate:

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

Care este cea mai bună practică pentru testarea funcțiilor anonime precum acestea? Care este minimul necesar pentru a asigura acoperirea testelor?

Răspuns #1

Poți pur și simplu să simulezi ca Observable să arunce un obiect de eroare ca Observable.throw({status: 404}) și să testezi blocul de eroare al observable-ului.

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

Actualizare 2019 :

Deoarece unii oameni sunt prea leneși să citească comentariile, permiteți-mi să pun asta aici: Este o practică recomandată să folosiți erorile pentru Rxjs

import { throwError } from 'rxjs'; // make sure to import the throwError from rxjs
const xService = fixture.debugElement.injector.get(SomeService);
const mockCall = spyOn(xService,'method').and.returnValue(throwError({status: 404}));

Actualizare 2022: Utilizarea throwError în modul menționat mai sus este depreciată. În schimb, folosește throwError(() => new Error({status: 404})):

import { throwError } from 'rxjs'; // make sure to import the throwError from rxjs
const xService = fixture.debugElement.injector.get(SomeService);
const mockCall = spyOn(xService,'method').and.returnValue(throwError(() => new Error({status: 404})));

Răspunsul #2

Nu sunt sigur exact care este scopul codului pe care îl arătați, care încearcă să testeze un serviciu mock. Problema acoperirii este cu componenta și cu callback-ul de eroare care nu a fost apelat (care este apelat doar atunci când apare o eroare).

Ceea ce fac de obicei pentru majoritatea serviciilor mele observabile este să creez un mock căruia metodele îi returnează pur și simplu pe el însuși. Serviciul mock are o metodă subscribe care acceptă callback-urile next, error și complete. Utilizatorul mock-ului poate să-l configureze pentru a adăuga o eroare, astfel încât funcția error să fie apelată, sau să adauge date, astfel încât metoda next să fie apelată. Ceea ce îmi place cel mai mult la asta este că totul este sincron.

Mai jos este ceva de genul a ceea ce folosesc de obicei. Este doar o clasă abstractă pentru ca alte mock-uri să o extindă. Oferă funcționalitatea de bază pe care o oferă un observable. Serviciul mock care se extinde ar trebui doar să adauge metodele de care are nevoie, returnându-se pe sine în metodă.

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

Acum, în testele tale, faci doar ceva de genul

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