在catchError(RxJS)上发生意外的toHaveBeenCalled

时间:2018-10-18 14:06:25

标签: angular rxjs karma-jasmine

我正在使用Angular 6 Tour of Heroes应用程序,并试图为HeroService.getHeroes()编写单元测试。

HeroService定义为:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

import { Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';

import { Hero } from './hero';
import { MessageService } from './message.service';

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

@Injectable({ providedIn: 'root' })
export class HeroService {

  private heroesUrl = 'api/heroes';  // URL to web api

  constructor(
    private http: HttpClient,
    private messageService: MessageService) { }

  /** GET heroes from the server */
  getHeroes (): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl)
      .pipe(
        tap(heroes => this.log('fetched heroes')),
        catchError(this.handleError('getHeroes', []))
      );
  }

 ...

  /**
   * Handle Http operation that failed.
   * Let the app continue.
   * @param operation - name of the operation that failed
   * @param result - optional value to return as the observable result
   */
  private handleError<T> (operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {

      // TODO: send the error to remote logging infrastructure
      console.error(error); // log to console instead

      // TODO: better job of transforming error for user consumption
      this.log(`${operation} failed: ${error.message}`);

      // Let the app keep running by returning an empty result.
      return of(result as T);
    };
  }

  /** Log a HeroService message with the MessageService */
  private log(message: string) {
    this.messageService.add(`HeroService: ${message}`);
  }
}

我的单元测试是:

import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

import { HeroService } from './hero.service';
import { MessageService } from './message.service';
import { Hero } from './hero';

const mockData = [
  { id: 1, name: 'Hulk' },
  { id: 2, name: 'Thor' },
  { id: 3, name: 'Iron Man' }
] as Hero[];

describe('Hero Service', () => {

  let heroService;
  let httpTestingController: HttpTestingController;

  beforeEach(() => {

    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule
      ],
      providers: [HeroService, MessageService]
    });
    httpTestingController = TestBed.get(HttpTestingController);

    this.mockHeroes = [...mockData];
    this.mockHero = this.mockHeroes[0];
    this.mockId = this.mockHero.id;
    heroService = TestBed.get(HeroService);
  });

  afterEach(() => {
    httpTestingController.verify();
  });

  it('should be created', () => {
    expect(heroService).toBeTruthy();
  });

  describe('getHeroes', () => {

    it('should return mock heroes', () => {
      spyOn(heroService, 'handleError');
      spyOn(heroService, 'log');

      heroService.getHeroes().subscribe(
        heroes => expect(heroes.length).toEqual(this.mockHeroes.length),
        fail
      );

      const req = httpTestingController.expectOne(heroService.heroesUrl);
      expect(req.request.method).toEqual('GET');
      req.flush(this.mockHeroes);

      expect(heroService.handleError).not.toHaveBeenCalled();
      expect(heroService.log).toHaveBeenCalledTimes(1);
    });
  });
});

测试失败:

Error message from Karma

尽管看起来确实确实调用了HeroService.handleError,但失败是无法预料的,​​在测试之外情况并非如此。为什么在测试执行期间会调用HeroService.handleError?我该如何纠正单元测试?

3 个答案:

答案 0 :(得分:0)

您编写的测试略有错误,测试应该更像这样(请注意间谍参考 logSpy errorSpy )。

it('should return mock heroes', () => {
  const errorSpy = spyOn(heroService, 'handleError').and.callThrough();
  const logSpy = spyOn(heroService, 'log');

  heroService.getHeroes().subscribe(
    (heroes: Hero[]) => {
      expect(heroes.length).toEqual(this.mockHeroes.length);
    }
  );

  const req = httpTestingController.expectOne(heroService.heroesUrl);
  //console.log(req);
  expect(req.request.method).toEqual('GET');
  req.flush(this.mockHeroes);

  expect(errorSpy).not.toHaveBeenCalled();
  expect(logSpy).toHaveBeenCalledTimes(1);
});

这是stackblitz,显示正在运行的测试,请尽情享受吧!

答案 1 :(得分:0)

it(`should get heroes`, () => {
    const handleErrorSpy = spyOn<any>(heroService, 'handleError').and.callThrough();
    const logSpy = spyOn<any>(heroService, 'log').and.callThrough();
    const addSpy = spyOn(messageService, 'add').and.callThrough();

    heroService.getHeroes().subscribe( (heroes: Hero[]) => {
      expect(heroes.length).toEqual(3);
    });

    const request = httpMock.expectOne( `api/heroes`, 'call to getHeroes');
    expect(request.request.method).toBe('GET');
    request.flush(mockHeroes);

    expect(handleErrorSpy).toHaveBeenCalledTimes(1);
    expect(logSpy).toHaveBeenCalledTimes(1);
    expect(addSpy).toHaveBeenCalledTimes(1);
    expect(handleErrorSpy).toHaveBeenCalledWith('getHeroes', [  ]);
    expect(logSpy).toHaveBeenCalledWith('fetched heroes');
    expect(addSpy).toHaveBeenCalledWith('HeroService: fetched heroes');
  });

答案 2 :(得分:0)

我找到了解决方案。在 hero.service.ts 中的 getHeroes() 中,将捕获错误的定义更改如下:

...
getHeroes (): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl)
      .pipe(
        tap(heroes => this.log('fetched heroes')),
        catchError(() => this.handleError('getHeroes', []))
      );
  }
...

我发现它感谢另一个问题 catchError always gets called in HTTP unit testing

编辑:使用之前的解决方案,检查请求返回错误情况的测试被破坏。该测试不在原始答案中,但它是文件 hero.service.spec.ts 的一部分:

it('should turn 404 into a user-friendly error', () => {
  ...
});

为了使这两个测试按预期运行,必须修改 hero.service.ts 中每个 catchError 中的代码(示例来自 getHeroes 函数):

...
catchError(err => this.handleError(err, 'getHeroes', []))
...

然后handleError函数如下:

private handleError<T>(err, operation = 'operation', result?: T) {
    console.log('handleError called. Operation:', operation, 'result:', result);
    console.error(err);
    this.log(`${operation} failed: ${err.message}`);
    return of(result as T);
}

TL;博士 去掉handleError里面的匿名函数。