Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 7 - catching HttpErrorResponse in unit tests

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.

like image 251
Marius Riehl Avatar asked Dec 04 '18 14:12

Marius Riehl


2 Answers

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...
}));
like image 66
kodi Avatar answered Oct 17 '22 11:10

kodi


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.

like image 7
Antony Avatar answered Oct 17 '22 10:10

Antony