I am trying to write unit tests for my API service but have some trouble catching HTTP errors. I am following this guide along with the Angular2 docs since the guide is (slightly) out of date in some minor areas.
All unit tests pass apart from those where an error is thrown by the service (due to error HTTP status code). I can tell this by logging out response.ok
. From what i've read this has something to do with the unit tests not executing asynchronously, hence, not waiting for the error response. However, I have no idea why this is the case here since I have used the async()
utility function in the beforeEach
method.
API Service
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);
}
Error unit test
// 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
});
Update 1
when I check the first callback, response.ok is undefined. This leads me to believe that there is something wrong in the setupConnections
utility.
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.");
}));
Update 2
If, instead of catching errors in the get method I do it explicitly in map then still have same problem.
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);
}
Update 3
After some discussion this issue submitted
You need to use it in the test case (the
it
). Whatasync
does is create an test zone that waits for all async tasks to complete before completing the test (or test area, e.g.beforeEach
).So the
async
in thebeforeEach
is only waiting for the async tasks to complete in the method before exiting it. But theit
also needs that same thing.UPDATE
Aside from the missing
async
, there seems to be a bug with theMockConnection
. If you look at themockRespond
, it always callsnext
, not taking into consideration the status codeThey have a
mockError(Error)
method, which is what callserror
but this does not call allow you to pass a
Response
. This is inconsistent with how the realXHRConnection
works, which checks for the status, and sends theResponse
either through thenext
orerror
, but is the sameResponse
Sounds like a bug to me. Something you should probably report. They should allow you to either send the
Response
in themockError
or do the same check in themockRespond
that they do in theXHRConnection
.Updated (by OP) SetupConnections()
Current solution