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.
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...
}));
Late answer, but may help someone facing similar issue.
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.
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');
});
});
});
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With