paulsm4
paulsm4

Reputation: 121719

Trying to run Angular HttpClient Jasmine test against live REST API: nothing happens

I have a simple "Contacts" REST service written in .Net Core. It works fine.

I'm trying to write an Angular 8.3 client to talk to it.

The first two things I did were:

  1. Create Contact.ts and Note.ts (corresponding to the REST models)
  2. Create an Angular service to communicate with the .Net Core REST API.

I thought maybe the best way to test the service would be to use the auto-generated unit test, contacts.service.spec.ts. I do NOT want to use a mock service: I want to use a "live" HttpClient so my Angular "contacts" service can talk directly to the .Net Core API.

It's not working: no errors, no warnings: the test just "passes" without even trying to send an HTTP message or wait for the HTTP response.

How can I debug my Angular service against the live REST service?

Can I use the contacts.service.spec.ts Jasmine test/Karma runner, or should I do "something else" to step through code execution?

models/contact.ts:

export class Contact {
  ContactId?: number;
  Name: string;
  EMail: string;
  Phone1: string;
  Phone2: string;
  Address1: string;
  Address2: string;
  City: string;
  State: string;
  Zip: string; 
}

models/note.ts

export class Note {
  NoteId?: number;
  Text: string;
  Date: Date;
  ContactId?: number;
}

services/contacts.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { retry, catchError } from 'rxjs/operators';
import { environment } from 'src/environments/environment';

import { Contact } from '../models/Contact';
import { Note } from '../models/Note';


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

  myAppUrl: string;
  myApiUrl: string;
  httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json; charset=utf-8'
    })
  };

  constructor(private http: HttpClient) {
    this.myAppUrl = 'http://localhost:53561/';  // environment.appUrl;
    this.myApiUrl = 'api/Contacts/';
  }

  getContacts(): Observable<Contact[]> {
    const url = this.myAppUrl + this.myApiUrl;
    return this.http.get<Contact[]>(url)
    .pipe(
      retry(1)
    );
  }
}

services/contacts.service.spec.ts

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ContactsService } from './contacts.service';

describe('ContactsService', () => {
  beforeEach(() => TestBed.configureTestingModule({
    imports: [HttpClientTestingModule],
    providers: [ContactsService]
  }));

  it('should be created', () => {
    const service: ContactsService = TestBed.get(ContactsService);
    expect(service).toBeTruthy();
  });

  it('should retrieve all contacts', () => {
    const contactsService: ContactsService = TestBed.get(ContactsService);
    let observable = contactsService.getContacts();
    expect(observable).toBeTruthy();
    debugger;
    observable.subscribe(data => {
      debugger;
      console.log("Done");
    },
    error => {
      debugger;
      console.error("observable error");
    });
  });
});

ng test

ng test


I've tried adding done() to my service test, and tried instantiating HttpClient inside the unit test. It's still not making any HTTP calls to the REST server :(

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { HttpClient } from '@angular/common/http';
import { ContactsService } from './contacts.service';

describe('ContactsService', () => {
  let httpClient: HttpClient;
  let service: ContactsService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [ContactsService, HttpClient]
    });
    // ERROR:
    //   "Timeout - Async callback was not invoked within 5000ms (set by jasmine.DEFAULT_TIMEOUT_INTERVAL)
    // Tried changing to 20000 - still getting Timeout...
    let originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000; // Getting timeout @default 5000
    httpClient = TestBed.get(HttpClient);
    //service = TestBed.get(ContactsService);
    service = new ContactsService(httpClient);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should retrieve all contacts', (done: DoneFn) => {
      service.getContacts().subscribe(data => {
        done();
    });
  });
});

Current error (despite manually updating timeout value inside of test)

Error: Timeout - Async callback was not invoked within 20000ms (set by jasmine.DEFAULT_TIMEOUT_INTERVAL)
Error: Timeout - Async callback was not invoked within 20000ms (set by jasmine.DEFAULT_TIMEOUT_INTERVAL)
    at <Jasmine>

Thanks to both Pytth and wessam yaacob. Here's how I got it working:

  1. Configure CORS on the .Net Core REST service

     public class Startup
       ...
       public void ConfigureServices(IServiceCollection services)
         ...
         services.AddCors(options => {
           options.AddPolicy("CorsPolicy",
             builder => builder.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader());
         });
       ...
       public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
         ...
         app.UseCors("CorsPolicy");
    

I was already doing thing - but it's useful to note here

  1. Use HttpClientModule and HttpClient instead of HttpClientTestingModule and HttpTestingController

    I never WANTED to use "HttpClientTestingModule", because I wanted to talk to the live service - not make a mocked call.

    I changed my code substantially along the way, but here's the unit test I finally wound up with:

     import { TestBed } from '@angular/core/testing';
     import { HttpClientModule, HttpClient, HttpErrorResponse } from '@angular/common/http';
    
     import { BlogPostService } from './blog-post.service';
    
     describe('BlogPostService', () => {
       let httpClient: HttpClient;
       let service: BlogPostService;
    
       beforeEach(() => {
         TestBed.configureTestingModule({
           imports: [HttpClientModule],
         });
         httpClient = TestBed.get(HttpClient);
         service = TestBed.get(BlogPostService);
       });
    
       it('should be created', () => {
         expect(service).toBeTruthy();
       });
    
       it('should retrieve blog posts', (done: DoneFn) => {
         service.getBlogPosts().subscribe(data => {
           done();
         });
       });
     });
    
  2. Final Note:

CORS does NOT seem to work UNLESS I used HTTPS:

    export class BlogPostService {
      ...
      constructor(private http: HttpClient) {
          this.myAppUrl = 'https://localhost:44330/'  // CORS error if 'http://localhost:44330/'
          this.myApiUrl = 'api/BlogPosts/';
          ...

PS: I know all about the "academic distinction" between "unit tests" and "integration tests". I was merely hoping the Angular "unit test" framework would give me the same convenience I have using static void main (String[] args) code in the Java world.

It turns out that's very definitely NOT the case...

I haven't tried an e2e test for this scenario yet - it was easier to just create a dummy page (a simple component) and use that for testing...

Upvotes: 3

Views: 3109

Answers (3)

Amnon
Amnon

Reputation: 2320

It is possible to instantiate a live HttpClient in a test setting by providing a custom HttpHandler.

import { XhrFactory } from "@angular/common"; // not the deprecated on in @angular/common/http
import { HttpClient, HttpXhrBackend } from '@angular/common/http';


class BrowserXhr implements XhrFactory {
    build(): any {
        return <any>(new XMLHttpRequest());
    }
}

let httpClient: HttpClient = new HttpClient(new HttpXhrBackend(new BrowserXhr()));
// now pass the HttpClient to some service

Upvotes: 0

wessam yaacob
wessam yaacob

Reputation: 937

If you are going to test your service against your real rest api so you have to replace HttpClientTestingModule with HttpClientModule

HttpClientTestingModule is just only used for mocking

aslo in your setup file , you have to add the test domain url in the accepted origin to avoid ->> CORS policy: No 'Access-Control-Allow-Origin'

in your case http://localhost:9876

   public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        { 
              ......
              app.UseCors(options =>
              options.WithOrigins("http://localhost:9876") 
              ......
        }

Upvotes: 3

Pytth
Pytth

Reputation: 4176

You need to use done that is provided by jasmine. Will edit to update:

https://codecraft.tv/courses/angular/unit-testing/asynchronous/#_jasmines_code_done_code_function


For your update: this should move you in the right direction

import { HttpClientTestingModule } from '@angular/common/http/testing';
import { HttpClient, HttpHeaders } from '@angular/common/http';

describe('LoginRepository', () => {
  let httpClient: HttpClient;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });
    service = TestBed.get(LoginRepository);
    httpClient = TestBed.get(HttpClient);
  });

    describe('#login', () => {
        it('makes http request', () => {
         spyOn(httpClient, 'post');
      //...

Upvotes: 1

Related Questions