我正在尝试为我的API服务编写单元测试,但在捕获HTTP错误时遇到一些问题。我正在关注此guide以及Angular2文档,因为指南在某些小区域(稍微)过时了。
所有单元测试都会与服务引发错误的单元测试分开(由于HTTP状态代码错误)。我可以通过退出response.ok
来说明这一点。从我所读到的,这与单元测试没有异步执行有关,因此,不等待错误响应。但是,我不知道为什么会出现这种情况,因为我在async()
方法中使用了beforeEach
效用函数。
get(endpoint: string, authenticated: boolean = false): Observable<any> {
endpoint = this.formatEndpoint(endpoint);
return this.getHttp(authenticated) // Returns @angular/http or a wrapper for handling auth headers
.get(endpoint)
.map(res => this.extractData(res))
.catch(err => this.handleError(err)); // Not in guide but should work as per docs
}
private extractData(res: Response): any {
let body: any = res.json();
return body || { };
}
private handleError(error: Response | any): Observable<any> {
// TODO: Use a remote logging infrastructure
// TODO: User error notifications
let errMsg: string;
if (error instanceof Response) {
const body: any = error.json() || '';
const err: string = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''}${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
// Imports
describe('Service: APIService', () => {
let backend: MockBackend;
let service: APIService;
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
BaseRequestOptions,
MockBackend,
APIService,
{
deps: [
MockBackend,
BaseRequestOptions
],
provide: Http,
useFactory: (backend: XHRBackend, defaultOptions: BaseRequestOptions) => {
return new Http(backend, defaultOptions);
}
},
{provide: AuthHttp,
useFactory: (http: Http, options: BaseRequestOptions) => {
return new AuthHttp(new AuthConfig({}), http, options);
},
deps: [Http, BaseRequestOptions]
}
]
});
const testbed: any = getTestBed();
backend = testbed.get(MockBackend);
service = testbed.get(APIService);
}));
/**
* Utility function to setup the mock connection with the required options
* @param backend
* @param options
*/
function setupConnections(backend: MockBackend, options: any): any {
backend.connections.subscribe((connection: MockConnection) => {
const responseOptions: any = new ResponseOptions(options);
const response: any = new Response(responseOptions);
console.log(response.ok); // Will return false during the error unit test and true in others (if spyOn log is commented).
connection.mockRespond(response);
});
}
it('should log an error to the console on error', () => {
setupConnections(backend, {
body: { error: `Some strange error` },
status: 400
});
spyOn(console, 'error');
spyOn(console, 'log');
service.get('/bad').subscribe(null, e => {
// None of this code block is executed.
expect(console.error).toHaveBeenCalledWith("400 - Some strange error");
console.log("Make sure an error has been thrown");
});
expect(console.log).toHaveBeenCalledWith("Make sure an error has been thrown."); // Fails
});
当我检查第一个回调时,response.ok未定义。这让我相信setupConnections
实用程序中存在错误。
it('should log an error to the console on error', async(() => {
setupConnections(backend, {
body: { error: `Some strange error` },
status: 400
});
spyOn(console, 'error');
//spyOn(console, 'log');
service.get('/bad').subscribe(res => {
console.log(res); // Object{error: 'Some strange error'}
console.log(res.ok); // undefined
}, e => {
expect(console.error).toHaveBeenCalledWith("400 - Some strange error");
console.log("Make sure an error has been thrown");
});
expect(console.log).toHaveBeenCalledWith("Make sure an error has been thrown.");
}));
如果我没有在get方法中捕获错误,而是在map中明确地执行它,那么仍然有同样的问题。
get(endpoint: string, authenticated: boolean = false): Observable<any> {
endpoint = this.formatEndpoint(endpoint);
return this.getHttp(authenticated).get(endpoint)
.map(res => {
if (res.ok) return this.extractData(res);
return this.handleError(res);
})
.catch(this.handleError);
}
经过一些讨论后this issue提交了
答案 0 :(得分:4)
这是我的工作解决方案,与上述建议类似,但更清晰:
it('should log an error to the console on error', async(inject([AjaxService, MockBackend], (
ajaxService: AjaxService, mockBackend: MockBackend) => {
service = ajaxService;
backend = mockBackend;
backend.connections.subscribe((connection: MockConnection) => {
const options: any = new ResponseOptions({
body: { error: 'Some strange error' },
status: 404
});
const response: any = new Response(options);
connection.mockError(response);
});
spyOn(console, 'error');
service.get('/bad').subscribe(res => {
console.log(res); // Object{error: 'Some strange error'}
}, e => {
expect(console.error).toHaveBeenCalledWith('404 - Some strange error');
});
})));
参考完整的工作代码:
以下是所有可能的测试方案。 注意:不要担心 AjaxService 。它是我在角度http服务上的自定义包装器,它被用作拦截器。
<强> ajax.service.spec.ts 强>
import { AjaxService } from 'app/shared/ajax.service';
import { TestBed, inject, async } from '@angular/core/testing';
import { Http, BaseRequestOptions, ResponseOptions, Response } from '@angular/http';
import { MockBackend, MockConnection } from '@angular/http/testing';
describe('AjaxService', () => {
let service: AjaxService = null;
let backend: MockBackend = null;
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
MockBackend,
BaseRequestOptions,
{
provide: Http,
useFactory: (backendInstance: MockBackend, defaultOptions: BaseRequestOptions) => {
return new Http(backendInstance, defaultOptions);
},
deps: [MockBackend, BaseRequestOptions]
},
AjaxService
]
});
}));
it('should return mocked post data',
async(inject([AjaxService, MockBackend], (
ajaxService: AjaxService, mockBackend: MockBackend) => {
service = ajaxService;
backend = mockBackend;
backend.connections.subscribe((connection: MockConnection) => {
const options = new ResponseOptions({
body: JSON.stringify({ data: 1 }),
});
connection.mockRespond(new Response(options));
});
const reqOptions = new BaseRequestOptions();
reqOptions.headers.append('Content-Type', 'application/json');
service.post('', '', reqOptions)
.subscribe(r => {
const out: any = r;
expect(out).toBe(1);
});
})));
it('should log an error to the console on error', async(inject([AjaxService, MockBackend], (
ajaxService: AjaxService, mockBackend: MockBackend) => {
service = ajaxService;
backend = mockBackend;
backend.connections.subscribe((connection: MockConnection) => {
const options: any = new ResponseOptions({
body: { error: 'Some strange error' },
status: 404
});
const response: any = new Response(options);
connection.mockError(response);
});
spyOn(console, 'error');
service.get('/bad').subscribe(res => {
console.log(res); // Object{error: 'Some strange error'}
}, e => {
expect(console.error).toHaveBeenCalledWith('404 - Some strange error');
});
})));
it('should extract mocked data with null response',
async(inject([AjaxService, MockBackend], (
ajaxService: AjaxService, mockBackend: MockBackend) => {
service = ajaxService;
backend = mockBackend;
backend.connections.subscribe((connection: MockConnection) => {
const options = new ResponseOptions({
});
connection.mockRespond(new Response(options));
});
const reqOptions = new BaseRequestOptions();
reqOptions.headers.append('Content-Type', 'application/json');
service.get('test', reqOptions)
.subscribe(r => {
const out: any = r;
expect(out).toBeNull('extractData method failed');
});
})));
it('should log an error to the console with empty response', async(inject([AjaxService, MockBackend], (
ajaxService: AjaxService, mockBackend: MockBackend) => {
service = ajaxService;
backend = mockBackend;
backend.connections.subscribe((connection: MockConnection) => {
const options: any = new ResponseOptions({
body: {},
status: 404
});
const response: any = new Response(options);
connection.mockError(response);
});
spyOn(console, 'error');
service.get('/bad').subscribe(res => {
console.log(res); // Object{error: 'Some strange error'}
}, e => {
expect(console.error).toHaveBeenCalledWith('404 - {}');
});
// handle null response in error
backend.connections.subscribe((connection: MockConnection) => {
connection.mockError();
});
const res: any = null;
service.get('/bad').subscribe(res, e => {
console.log(res);
}, () => {
expect(console.error).toHaveBeenCalledWith(null, 'handleError method with null error response got failed');
});
})));
});
<强> ajax.service.ts 强>
import { Injectable } from '@angular/core';
import { Http, Response, RequestOptionsArgs, BaseRequestOptions } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
/**
* Wrapper around http, use this for all http operations.
* It has centralized error handling as well.
* @export
* @class AjaxService
*/
@Injectable()
export class AjaxService {
/**
* Creates an instance of AjaxService.
* @param {Http} http
*
* @memberOf AjaxService
*/
constructor(
private http: Http,
) { }
/**
* Performs a request with get http method.
*
* @param {string} url
* @param {RequestOptionsArgs} [options]
* @returns {Observable<Response>}
*
* @memberOf AjaxService
*/
get(url: string, options?: RequestOptionsArgs): Observable<Response> {
options = this.getBaseRequestOptions(options);
options = this.setHeaders(options);
return this.http.get(url, options)
.map(this.extractData)
.catch(this.handleError);
}
/**
* Performs a request with post http method.
*
* @param {string} url
* @param {*} body
* @param {RequestOptionsArgs} [options]
* @returns {Observable<Response>}
*
* @memberOf AjaxService
*/
post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
options = this.getBaseRequestOptions(options);
options = this.setHeaders(options);
return this.http.post(url, body, options)
.map(this.extractData)
.catch(this.handleError);
}
/**
* Util function to fetch data from ajax response
*
* @param {Response} res
* @returns
*
* @memberOf AjaxService
*/
private extractData(res: Response) {
const body = res.json();
const out = body && body.hasOwnProperty('data') ? body.data : body;
return out;
}
/**
* Error handler
* Future Scope: Put into remote logging infra like into GCP stackdriver logger
* @param {(Response | any)} error
* @returns
*
* @memberOf AjaxService
*/
private handleError(error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''}${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
/**
* Init for RequestOptionsArgs
*
* @private
* @param {RequestOptionsArgs} [options]
* @returns
*
* @memberOf AjaxService
*/
private getBaseRequestOptions(options: RequestOptionsArgs = new BaseRequestOptions()) {
return options;
}
/**
* Set the default header
*
* @private
* @param {RequestOptionsArgs} options
* @returns
*
* @memberOf AjaxService
*/
private setHeaders(options: RequestOptionsArgs) {
if (!options.headers || !options.headers.has('Content-Type')) {
options.headers.append('Content-Type', 'application/json');
}
return options;
}
}
答案 1 :(得分:3)
据我所知,这与单元测试没有异步执行有关,因此,不等待错误响应。但是,我不知道为什么会出现这种情况,因为我在
public Observable<CopyOnWriteArrayList<Entity>> getEntities() {
方法中使用了async()
效用函数
您需要在测试用例中使用它(beforeEach
)。 it
所做的是创建一个测试区域,等待所有异步任务在完成测试(或测试区域,例如async
)之前完成。
因此beforeEach
中的async
只是等待异步任务在退出之前在方法中完成。但beforeEach
也需要同样的事情。
it
除了遗失的it('should log an error to the console on error', async(() => {
}))
之外,MockConnection
似乎存在错误。如果您查看mockRespond
,它始终会调用async
,而不会考虑状态代码
next
他们有一个mockError(Error)
方法,即调用mockRespond(res: Response) {
if (this.readyState === ReadyState.Done || this.readyState === ReadyState.Cancelled) {
throw new Error('Connection has already been resolved');
}
this.readyState = ReadyState.Done;
this.response.next(res);
this.response.complete();
}
error
但这不会调用允许您传递mockError(err?: Error) {
// Matches ResourceLoader semantics
this.readyState = ReadyState.Done;
this.response.error(err);
}
。这与真实XHRConnection
的工作方式不一致,后者会检查状态,并通过Response
或Response
发送next
,但是error
相同}}
Response
对我来说听起来像个错误。你应该报告的事情。他们应该允许您在response.ok = isSuccess(status);
if (response.ok) {
responseObserver.next(response);
// TODO(gdi2290): defer complete if array buffer until done
responseObserver.complete();
return;
}
responseObserver.error(response);
中发送Response
,或者在mockError
中对mockRespond
进行同样的检查。
当前解决方案
XHRConnection