我正在使用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);
});
});
});
测试失败:
尽管看起来确实确实调用了HeroService.handleError
,但失败是无法预料的,在测试之外情况并非如此。为什么在测试执行期间会调用HeroService.handleError
?我该如何纠正单元测试?
答案 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里面的匿名函数。