Search code examples
http-postkarma-jasmineangular17httptestingcontroller

Angular testing - subscribing to HTTP response observable triggers a new request


Context

I have a service that is responsible for sending a POST request for form data to my backend.

The method for sending the requests decides depending on the class instance of the form data which endpoint will be used. It then sends an HTTP POST request to the backend and uses the pipe function to catch errors. The service itself has some logic depending on the response of the backend so it subscribes to the observable itself, while also returning a reference to the same observable, so that calling components can react to the response themselves.

/*
...
*/
export class MyService {
    ROUTES = {
      'A': '/api/a/edit',
      'B': '/api/b/edit',
    }
    /*
    ...
        */
    private getType(o: Publication | CTMSMilestone): keyof typeof ROUTES | undefined {
        return o instanceof ClassA? "A" :
              o instanceof ClassB? "B" : undefined
    }

    public postObject(o: ClassA | ClassB){
        const type = this.getType(o)
        if (!type) {
          console.error('Object type not supported for POST request.', o);
          return of(false)
        }
        const observable = this.http.post(ROUTES[type], o)
          .pipe(catchError(this.feedbackService.notifyError()))

        observable.subscribe(_ => {
          /*
            Some logic needed
          */
        })
        return observable
    }
}

When testing that service, I pass data of each class into the method and catch the response, trying to figure out if the method sends the data to the right endpoint.

describe('MyService', () => {
  let service: MyService;
  let httpTestingController: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        HttpClientModule,
        HttpClientTestingModule
      ],
    });
    service = TestBed.inject(MyService);
    httpTestingController = TestBed.inject(HttpTestingController);

  });

  it('should post object to the right URL', () => {
    const URLS = {
      A: generateURL('/api/a/edit'),
      B: generateURL('/api/b/edit'),
    }

    let postObject: ClassA | ClassB = new A()

    service.postObject(postObject)

    httpTestingController.expectNone(URLS.A)
    httpTestingController.expectOne(URLS.B).flush(true)

    postObject = new B()

    service.postObject(postObject)

    httpTestingController.expectNone(URLS.A)
    httpTestingController.expectOne(URLS.B).flush(true)
    httpTestingController.verify()
  })
});

function generateURL(path: string) {
  const request = new HttpRequest('POST', path, null)
  return request.url
}

Problem

The example above does work fine, but I get the annoying message that the SPEC HAS NO EXPECTATIONS.

So I tried to react to the response by subscribing to the returned observable:

service.postObject(postObject)subscribe(status => {
  expect(status).toBe(true)
  httpTestingController.verify()
});

By doing so, the httpTestingController throws an error:

Expected one matching request for criteria "Match URL: /api/a/edit", found 2 requests

By adding the expression

console.log(httpTestingController.match(() => true))

I was able to see, that not one, but two requests for /api/a/edit were posted, even tho the only call to post happens inside the postObject method.

How can this happen? Why should any kind of subscription trigger the source of the observable??

Versions

Just a quick list of my packages installed.


Solution

  • I really don't know why I haven't been able to find it sooner... But the Angular documentation clearly states that the HttpClient's methods.

    Construct[s] an observable that, when subscribed, causes the configured METHOD_NAME request to execute on the server.

    I am still bugged out by this. But there are many ways to go aournd this behaviour, by either parsing the returned Observable (o) to a Promise (which itself subscribes to the Response, so that the request is triggered) with maybe the firstValueFrom(o) or the o.toPromise() method (or whatever else the rxjs package is equipped with). A non-promise ways is to trigger a Subject (s) when the HttpClient sends the servers response, and then subscripe to the s.asObservable() return value.