0

WHAT: Testing an async function that uses await

WITH: Angular 9, Jasmine 3.4.0

Minimal code to reproduce: StackBlitz

I have a function like this. Note, that it has to await this.getHeaders(). Whenever I remove await and replace the implementation of getHeaders() by some synchonous implementation, the test runs successfully.

What is the correct way to test it?

private async loadUserData() {
    // Try to remove await - the test runs successfully
    //const headers = this.getHeaders();
    const headers = await this.getHeaders();
    return this.httpClient
      .get<UserData>(window.location.origin + '/12345', { headers })
      .toPromise()
      .then((data) => {
        this.userData = data;
      })
      .catch((e) => {
        console.log('Oh noooo...');
      });
  }

WHAT I'VE TRIED:

  • It might be the case that the url in StackBitz is not correct, but when testing locally I am sure it is, so this should not be the root cause
  • not sure, if fakeAsync() will help - the "pure" http test works...

NOT A DUPE OF:

Maria K.
  • 219
  • 1
  • 8

1 Answers1

1

You're very close. Your test is now structured properly but you need some testing utilities provided by Angular to make sure that your Promises are resolved in the correct order for your expectations to be correct.

Ultimately the problem relates to Angular zones. Because you're structuring promises in your service, and that promise resolution must occur before an outgoing request occurs. You call loadUserData in your test, and then on the next line you write an assertion that says "make sure that this request happened, if not that's an error". When you write your header retrieval function in such a way that you don't use an async primitive (like Promise or Observable) this request happens "immediately". But when you use a Promise, no request happens "immediately". Instead, it has to resolve your header retrieval function first.

Luckily, your test failing is just a phantom feature of the test environment and not a bug in your implementation. Like I said, Angular gives you some testing tools to make sure you can "flush" all pending promises before writing an assertion.

import { fakeAsync,  tick } from '@angular/core/testing';

// instead of using `done`, which is just fine btw, we wrap our test function
// in fakeAsync(). This let's us have fine grained control over stepping through
// our code under test
it('should return user data on success', fakeAsync(() => {
  const mockUserData : UserData = {
    name: 'John',
      surname: 'Do',
      id: '12345'
    };

    (service as any).loadUserData().then(() => {
      expect((service as any).userData).toEqual(mockUserData);
      //expect(req.request.method).toEqual('GET');
    });

    // tick() steps through the next round of pending asynchronous activity
    // this will also step through 'setTimeout' and 'setInterval' code
    // you may have in your service, as well as Observable code
    tick();
    const req = httpTestingController.expectOne(
      'https://testing-qh48mg.stackblitz.io/12345',
    );
    req.flush(mockUserData);
}));

Updated Stackblitz. Docs on fakeAsync. Related question on testing Http code in Angular using tick.

xtianjohns
  • 706
  • 3
  • 12
  • Thank you very much! This works for me. Next level - testing a deeper nesting - I have a function that additionally awaits in the then() part of the http-request (after turning it to a promise): return this.http .get(this.requestUrl, { headers, responseType: 'text' }) .toPromise() .then(async (url) => { const someThing = await this.someFunction(); Gonna dive deeper into tick() and fakeAsync... – Maria K. May 30 '20 at 08:43