[英]Angular 2 Unit Testing Observable Errors (HTTP)
我正在嘗試為我的API服務編寫單元測試,但是在捕獲HTTP錯誤時遇到了一些麻煩。 我在Angular2文檔中關注本指南 ,因為該指南在某些次要領域(略)過時了。
所有單元測試都通過了服務拋出錯誤的測試(由於錯誤HTTP狀態代碼)。 我可以通過注銷response.ok
來說明這一點。 從我讀到的內容來看,這與單元測試不是異步執行有關,因此不必等待錯誤響應。 但是,我不知道為什么會出現這種情況,因為我在beforeEach
方法中使用了async()
實用程序函數。
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);
}
經過一番討論, 該問題提交了
這是我的工作解決方案,與上述建議類似,但更加清晰:
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;
}
}
從我讀到的內容來看,這與單元測試不是異步執行有關,因此不必等待錯誤響應。 但是,我不知道為什么會出現這種情況,因為我在
beforeEach
方法中使用了async()
實用程序函數
您需要在測試用例( it
)中使用它。 async
作用是創建一個測試區域,等待所有異步任務完成,然后再完成測試(或測試區域,例如beforeEach
)。
因此, async
的beforeEach
退出之前只等待異步任務完成的方法。 但是it
也需要同樣的東西。
it('should log an error to the console on error', async(() => {
}))
除了缺少async
, MockConnection
似乎還有一個錯誤。 如果您看一下mockRespond
,它將始終調用next
,而不考慮狀態碼
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();
}
他們有一個mockError(Error)
方法,這就是所謂的error
mockError(err?: Error) {
// Matches ResourceLoader semantics
this.readyState = ReadyState.Done;
this.response.error(err);
}
但這並不允許您傳遞Response
。 這與實際XHRConnection
工作方式不一致,后者會檢查狀態,並通過next
或error
發送Response
,但是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
。
當前解決方案
function setupConnections(backend: MockBackend, options: any): any {
backend.connections.subscribe((connection: MockConnection) => {
const responseOptions: any = new ResponseOptions(options);
const response: any = new Response(responseOptions);
// Have to check the response status here and return the appropriate mock
// See issue: https://github.com/angular/angular/issues/13690
if (responseOptions.status >= 200 && responseOptions.status <= 299)
connection.mockRespond(response);
else
connection.mockError(response);
});
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.