问题
i'm currently learning Angular 7 (haven't used any previous version) and encountered something i couldn't fix when writing unit tests for a service.
I have a service that gets JSON from REST and parses it into a Class. Referring to the Angular Docs, i wrote a test using HttpClientSpy to simulate a 404 Error.
What happens: The Test Fails with the Error Message: "expected data.forEach is not a function to contain '404'"
So the Service gets the HttpErrorResponse as Input but tries to parse it like it was a regular response in the map function. This fails, catchError is called and the data.forEach is not a function Error is thrown.
Expected behavior: i would expect that map() is not executed and it should jump directly into the catchError function.
How i fixed it (for now): Adding the following lines of code to the map function of the service makes the test work.
if (data instanceof HttpErrorResponse)
throw new HttpErrorResponse(data);
The test:
it('should throw an error when 404', () => {
const errorResponse = new HttpErrorResponse({
error: '404 error',
status: 404, statusText: 'Not Found'
});
httpClientSpy.get.and.returnValue(of(errorResponse));
service.getComments().subscribe(
fail,
error => expect(error.message).toContain('404')
);
});
The service:
getComments(): Observable<CommentList> {
return this.http
.get('https://jsonplaceholder.typicode.com/comments')
.pipe(
map((data: Array<any>) => {
let t: Array<Comment> = [];
data.forEach(comment => {
if(!('id' in comment) || !('body' in comment) || !('email' in comment) || !('name' in comment))
throw new Error("Could not cast Object returned from REST into comment");
t.push(<Comment>{
id: comment.id,
body: comment.body,
author: comment.email,
title: comment.name,
});
});
return new CommentList(t);
}),
catchError((err: HttpErrorResponse) => {
return throwError(err);
})
);
}
Am i getting something wrong? I think the expected behavior is what i should experience, at least thats how i interpret the Angular docs.
回答1:
Late answer, but may help someone facing similar issue.
Root cause
The Error Message: "expected data.forEach is not a function to contain '404'" is because of the of
operator in the test case:
httpClientSpy.get.and.returnValue(of(errorResponse));
The of
operator returns an observable that emits the arguments.
This is useful when you want to return data, but not when you want to raise a 404 error.
In order for the spy to raise error, the response should reject and not resolve.
Solution 1
This solution uses the defer
RxJS operator along with jasmine.createSpyObj
approach you used in your example.
import { TestBed } from '@angular/core/testing';
import { HttpErrorResponse } from '@angular/common/http';
import { defer } from 'rxjs';
import { CommentsService } from './comments.service';
// Create async observable error that errors
// after a JS engine turn
export function asyncError<T>(errorObject: any) {
return defer(() => Promise.reject(errorObject));
}
describe('CommentsService', () => {
let httpClientSpy: { get: jasmine.Spy };
let service: CommentsService;
beforeEach(() => {
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
service = new CommentsService(httpClientSpy as any);
});
it('should throw an error when 404', () => {
const errorResponse = new HttpErrorResponse({
error: '404 error',
status: 404,
statusText: 'Not Found'
});
httpClientSpy.get.and.returnValue(asyncError(errorResponse));
service.getComments().subscribe(
data => fail('Should have failed with 404 error'),
(error: HttpErrorResponse) => {
expect(error.status).toEqual(404);
expect(error.error).toContain('404 error');
});
});
});
Solution 2
It is better to use the angular HttpClientTestingModule to test the HttpClient
usage. The following example shows the same test using HttpClientTestingModule
.
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpErrorResponse } from '@angular/common/http';
import { CommentsService } from './comments.service';
describe('CommentsService test using HttpClientTestingModule', () => {
let httpTestingController: HttpTestingController;
let service: CommentsService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ]
});
httpTestingController = TestBed.get(HttpTestingController);
service = TestBed.get(CommentsService);
});
it('throws 404 error', () => {
service.getComments().subscribe(
data => fail('Should have failed with 404 error'),
(error: HttpErrorResponse) => {
expect(error.status).toEqual(404);
expect(error.error).toContain('404 error');
}
);
const req = httpTestingController.expectOne('https://jsonplaceholder.typicode.com/comments');
// Respond with mock error
req.flush('404 error', { status: 404, statusText: 'Not Found' });
});
});
The angular HTTP Testing documentation explains this approach.
Note: The examples are tested using Angular v8.
回答2:
A late answer, a slightly different way but this works too.
it('should show modal if failed', inject([Router], (mockRouter: Router) => {
const errorResponse = new HttpErrorResponse({
error: { code: `some code`, message: `some message.` },
status: 400,
statusText: 'Bad Request',
});
spyOn(someService, 'methodFromService').and.returnValue(throwError(errorResponse));
expect...
expect...
expect...
}));
来源:https://stackoverflow.com/questions/53615585/angular-7-catching-httperrorresponse-in-unit-tests