Search code examples
angulartypescriptrxjsobservableangular-httpclient

@angular/common/http get-Method does not return the Observable with the provided <type> - instanceof that type fails


This is my Service-class job.service.ts. It is basically calling a Express app and getting a Job-object as result. Which it is returning as Observable to the caller of the getJob() method.

import { Injectable } from '@angular/core';
import {Job} from "./domain-models/Job";
import {Observable} from 'rxjs';
import {HttpClient} from "@angular/common/http";

@Injectable({
  providedIn: 'root'
})
export class JobService {

  readonly ROOT_URL = 'http://localhost:3000';
  constructor(private http: HttpClient) { }

  getJob(jobId: string): Observable<any> {
    if(jobId){
  
      return this.http.get<Job>(`${this.ROOT_URL}/jobs/${jobId}`);
    }else{
        return new Observable<Error>( subscriber => {
          subscriber.next(new Error('No jobID provided));
        })

      }
    }
  }

This is a spec from my JobService test class. with the spyOn method I intercept every attempt to call the real API and return a fake response which apparently is an instance of Job. job.service.spec.ts:

  describe('Testing the getJob() function of JobService', ()=>{


beforeEach(()=>{
      let jobId: string = 'A123456789'
      const job: Job = {
        actualCost: 100,
        amountPaid: 50,
        wishAnalysis: false
      };
      spyOn(httpService, "get").and.returnValue(of<Job>(job));
    })
 


it('should return an Observable with the corresponding job object matching the id',()=>{
      let jobIdPassed: string = 'A123456789'
      service.getJob(jobIdPassed).subscribe(value => {
        expect(value instanceof Job).toBeTrue();
      });
    })
})

If I execute

expect(value instanceof Job).toBeTrue();

the test fails because it seems that value is not an instance of Job. If I change the JobServices getJob() function and return a manually created Observable like so:

getJob(jobId: string): Observable<any> {
    if(jobId){
      return new Observable<Job>(subscriber => {
         subscriber.next(new Job());
         subscriber.complete();

      })
    }else{
        return new Observable<Error>( subscriber => {
          subscriber.next(new Error('No jobID provided));
        })

      }
    }

The test works! So the problem is why is HttpClient librarys get() method not providing the response in the appropriate type inside an Observable like it's supposed to?

I appreciate that you took the time to read it and for any help!


Solution

  • Problem 1: Incorrect setup of test data

    The job you create in your test is not the instance of Job class:

    class Job {
      constructor(public actualCost: number, 
                  public amountPaid: number, 
                  public wishAnalysis: boolean) {}
    }
    
    const job: Job = {
      actualCost: 100,
      amountPaid: 50,
      wishAnalysis: false
    };
    
    console.log(job);
    console.log(job instanceof Job); //false
    

    Problem 2: Incorrect assumptions about http.get

    This is a common pitfall - http.get<T> is only a convenience generic

    http.get<T> returns parsed json object.

    T is used only for convenience, the expectation is that it describes the shape of your data correctly. But no runtime check is performed - missing properties, additional properties, nothing will be checked (and fail later in runtime, when you access the data).

    In particular, if T is a class, you are bound to have issues, as the returned data will not be the instance of T in runtime - it may have the fields filled in, if names match, but prototype chain is not set up. This means that

    • method calls will fail in runtime
    • instanceof check fails

    Solution

    Don't use a class to model the shape of data aquired with http.get. Use a type (or interface) instead. If you need a class, map the returned data.