angular: HttpTestingController.expectOne() not working with queryparameters

I’m submitting a…


[ ] Regression (a behavior that used to work and stopped working in a new release)
[X] Bug report  
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Related to #19903 When testing a request that contains query parameters, the test throws 2 errors:

  1. Expected one matching request for criteria “Match URL: ./assets/mock/modules.json”, found none.
  2. Expected no open requests, found 1: GET ./assets/mock/modules.json

lgbwi

Appending the paramters to the expected URL does not work either.

Expected behavior

Test failure message should show the full path including parameters. OR We should be able to specify expected parameters or ignore all.

Minimal reproduction of the problem with instructions

[example.service.spec.ts]

import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injector } from '@angular/core';
import { async, TestBed } from '@angular/core/testing';
import { ExampleService } from './example.service';

fdescribe('Create a test suite for the Example Service', () => {
    let injector: Injector;
    let exampleService: ExampleService;
    let httpMock: HttpTestingController;

    beforeEach(() => {
        injector = TestBed.configureTestingModule({
            imports:
                [HttpClientTestingModule],
            providers:
                [ExampleService]
        });
        exampleService = injector.get(ExampleService);
        httpMock = injector.get(HttpTestingController);
    });

    afterEach(() => {
        httpMock.verify();
    });

    it('should not immediately connect to the server', () => {
        httpMock.expectNone({});
    });

    describe('when fetching all stuff', () => {
        it('should make a GET request', async(() => {
            exampleService.getStuff();

            let req = httpMock.expectOne(`./assets/mock/modules.json`);
            expect(req.request.method).toEqual('GET');
            req.flush([]);
        }));
    });
});
[example.service.ts]

import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable()
export class ExampleService {

    constructor(private http: HttpClient) {
    }

    getStuff() {
        let params = new HttpParams().append('testparam', 'testvalue')
        return this.http.get('./assets/mock/modules.json', {params}).toPromise().then(value => {
            return value
        })
    }
}

What is the motivation / use case for changing the behavior?

Currently it is impossible to test http requests in a service which contain query parameters.

Environment


Angular version: 4.4.4


Browser:
- [X] Chrome (desktop) version 61.0.3163.100
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
 
For Tooling issues:
- Node version: V8.4.0  
- Platform: Windows  

Others:

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 78
  • Comments: 31 (10 by maintainers)

Commits related to this issue

Most upvoted comments

I just came across the same problem. Fixing the error message would be one improvement, but in my view it would be even better if it matched against request.url rather than request.urlWithParams. I’d rather write:

const req = httpMock.expectOne({ method: 'GET', url: 'http://example.org' });
expect(req.request.params.get('foo')).toEqual('bar');

than:

httpMock.expectOne({ method: 'GET', url: 'http://example.org?foo=bar' });

not least because it avoids any ordering issues when you have more than one parameter.


A current workaround is to use the request matcher function instead:

const req = httpMock
    .expectOne(req => req.method === 'GET' && req.url === 'http://example.org');

@JrPribs this is the same problem I was having just now. I was already beginning to assume that expectOne() is buggy, but it’s not.

  • expectOne() should also test httpParams as they might be important in some cases
  • you can implement a simple matcher that matches request.url instead of request.urlWithParams if you don’t care
  • If you pass a URL without httpParams to expectOne() it can’t print any params when no request matches. It doesn’t know any params.
  • most importantly: verify() should print httpParams. Reading "No match found for https://my-url.com" on the one hand and "Open request found for https://my-url.com" on the other hand is very confusing!

Just ran into this as well. What you could do however is to use the following check

  it('should add HTTP params', () => {
    service.fetchAll({ name: 'Juri' }).subscribe();

    const req = httpTestingController.expectOne(req => req.params.has('name'));

    req.flush({});
  });

Or in a more complex case, where for instance I check whether my service method removes all null/undefined/empty values from the query url, I simply check whether it’s the current URL in the expectOne and then I access the request and add expect statements for the individual params.

  it('should remove null/undefined/empty values from HTTP params', () => {
    service
      .fetchAll({ name: 'Juri', age: null, street: undefined, location: '' })
      .subscribe();

    const req = httpTestingController.expectOne(req => req.url.includes('district'));

    expect(req.request.params.has('name')).toBeTruthy();
    expect(req.request.params.has('age')).toBeFalsy();
    expect(req.request.params.has('street')).toBeFalsy();
    expect(req.request.params.has('location')).toBeFalsy();

    req.flush({});
  });

Spent over an hour trying to get a single test with a singe query param to work to no avail, have tried all solutions above. Now I am getting…

Error: Expected one matching request for criteria "Match by function: ", found none.

Other tests without query params work.

Shame, cannot spend any more time on this, test commented out in my code 😦

@algorys the downside of the latter approach is that it is dependent on the order of query parameters being added to the URL, which I don’t think is guaranteed if you’re building the URL from a params object and probably shouldn’t be relied on. Obviously if you only have one query parameter that isn’t a problem, but it doesn’t scale. That’s why I suggested checking the URL without query params, then validating those separately.

I wrote this pair of utility functions which should provide a more robust solution for testing requests with parameters:

import { HttpParams, HttpRequest } from '@angular/common/http';

export function extractParams(url: string): HttpParams {
  let queryString: string;
  if (url) {
    const qIndex = url.indexOf('?');
    queryString = qIndex >= 0 ? url.slice(qIndex + 1) : '';
  } else {
    queryString = '';
  }

  return new HttpParams({fromString: queryString});
}


export function requestMatcher(match: {url?: string, method?: string, params?: HttpParams | {[key: string]: string | string[]}}): (HttpRequest) => boolean {
  const expectedParams = match.params && (match.params instanceof HttpParams ? match.params : new HttpParams({fromObject: match.params}));
  return (request: HttpRequest<any>) => {
    if (match.method && request.method !== match.method.toUpperCase()) {
      return false;
    }

    if (match.url && match.url !== match.url.split('?')[0]) {
      return false;
    }

    if (expectedParams) {
      const actualParams = extractParams(request.urlWithParams);
      const allExpectedParamsMatch = expectedParams.keys().every(key => {
        const expected = expectedParams.getAll(key);
        const actual = actualParams.getAll(key);
        return expected && actual && 
            expected.length === actual.length && 
            expected.every(((value, index) => value === actual[index]));
      });

      if (!allExpectedParamsMatch) {
        return false;
      }
    }

    return true;
  };
}
const request = httpTestingController.expectOne(requestMatcher({url: '/foo/bar', params: {'test', 'true'}}));

// This will give an HttpParams with all the params, not just the ones explicitly passed to HttpClient.request
const params = extractParams(request.request.urlWithParams);

YMMV, they helped me with my test cases.

Edit: fixed bugs pointed out by textbook below.

@samih-dev it’s not clear how that’s relevant to this issue. Per this comment by the OP it’s not about promise vs. observable, and my suggestion is independent of either.

This is quite annoying, just spent some hours until figuring this should be a bug in expectOne. And here it is.

@textbook work-around works fine, thank you.

@marcellarius this looks suspicious:

const expected = expectedParams.getAll(key);
const actual = expectedParams.getAll(key);
return expected && actual && expected.every(value => actual.includes(value));

actualParams isn’t used there at all, expected and actual are the same array. You should re-review your tests (and the code they cover), they aren’t telling you what you think they are.

This behaviour is absolutely broken, undocumented and unexpected.

It’s not just an issue with the HttpTestingController though; if you make a request with a querystring baked into the URL, the params object in the request doesn’t get populated, and the request.url will contain these query params. The issue there seems to be the HttpRequest constructor logic. Ideally the params should reflect the actual URL params, including any that were already in the url string.

My ideal expected behaviour would be to parse the expected URL, then ensure everything matches the request made – any query string parameters specified in the test expectation would be compared against the actual URL; any not specified would be ignored.

As seems to be usual with Angular, these caveats are not mentioned anywhere in the documentation.

I have run into this issue when I have a typo in my expectation when using params. Should the HttpTestingController not indicate that a different url was call than expected, or am I causing it to throw the nondescript error because of my implementation? I was under the impression that .verify() would let me know if the wrong url was called, am I mistaken?

Same thing happens to me for my project:

Class:

import {Injectable} from '@angular/core';
import {HttpClient, HttpHeaders, HttpParams} from "@angular/common/http";
import {Observable} from "rxjs/Observable";


const BACKEND_PAGINATION_LIMIT = 25;

@Injectable()
/**
 * Class who make requests on Alignak backend
 * Injectable service
 */
export class BackendClient {
  token: string;
  url: string;

  /**
   * @param {HttpClient} http - http client for requests
   */
  constructor(public http: HttpClient) {
    this.updateData()
  }

  /**
   * Update data of backend: {@link url} and {@link token}
   */
  private updateData(){
    this.token = localStorage.getItem('token');
    this.url = localStorage.getItem('url');
  }

  /**
   * GET http function
   * @param {string} endpoint - endpoint of request
   * @param {HttpParams} params - http parameters of request
   * @param {HttpHeaders} headers - http headers of request
   * @returns {Observable<Object>} - observable object
   */
  private get(endpoint: string, params?: HttpParams, headers?: HttpHeaders): Observable<Object> {
    this.updateData();
    if (headers == null){
      headers = new HttpHeaders()
        .set('Accept', 'application/json')
        .set('Authorization', this.token);
    }
    return this.http.get(
      this.url + '/' + endpoint, {headers, params}
    )
  }

  /**
   * Return host data for given endpoint
   * @param {string} endpoint - endpoint of request, default is "host"
   * @returns {Observable<any>} - observable object
   */
  public getHosts(endpoint = 'host'): Observable<any> {
    let params = new HttpParams()
      .set('where', JSON.stringify({'_is_template': false}))
      .set('max_results', JSON.stringify(BACKEND_PAGINATION_LIMIT
    ));

    return this.get(endpoint, params)
  }
}

Test:

import {TestBed, getTestBed} from '@angular/core/testing';
import {HttpHeaders, HttpParams} from "@angular/common/http";
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

import {BackendClient} from "./client.service";

describe('BackendClient Service', () => {

  let injector: TestBed;
  let service: BackendClient;
  let httpMock: HttpTestingController;

  beforeEach(() => {

    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [BackendClient]
    });
    injector = getTestBed();
    localStorage.setItem('token', 'my-long-token');
    localStorage.setItem('url', 'http://demo.alignak.net:5000');
    service = injector.get(BackendClient);
    httpMock = injector.get(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('Get Hosts from Backend', () => {
    const dummyHosts = [
      {_items: 'Hosts list'}
    ];
    const dummyParams = new HttpParams()
      .append('where', JSON.stringify({'_is_template': false}))
      .append('max_results', JSON.stringify(25));

    service.getHosts().subscribe(
      hosts => {
        expect(hosts.length).toBe(1);
    });

    // ERROR HERE:
    // If I do the following, test complains about not found request but find the same request url name:
    const req = httpMock.expectOne(`${service.url}/host`);
    // FIX: So change to following code fixes issue:
    const req = httpMock.expectOne(req => req.method === 'GET' && req.url === `${service.url}/host`);
    expect(req.request.params.get('where')).toEqual('{"_is_template":false}');
    req.flush(dummyHosts);
  });
});

Only the fix in comment help me to solve this problem. ERROR output:

Chrome 66.0.3359 (Linux 0.0.0) BackendClient Service Get Hosts from Backend FAILED
	Error: Expected one matching request for criteria "Match URL: http://demo.alignak.net:5000/host", found none.
	    at HttpClientTestingBackend.expectOne (webpack:///node_modules/@angular/common/esm5/http/testing.js:419:0 <- test-config/karma-test-shim.js:138937:19)
	    at UserContext.<anonymous> (webpack:///src/backend/client.service.spec.ts:88:25 <- test-config/karma-test-shim.js:138498:28)
	    at ZoneDelegate.invoke (webpack:///node_modules/zone.js/dist/zone.js:388:0 <- test-config/karma-test-shim.js:123739:26)
	    at ProxyZoneSpec.onInvoke (webpack:///node_modules/zone.js/dist/proxy.js:128:0 <- test-config/karma-test-shim.js:126726:39)
	    at ZoneDelegate.invoke (webpack:///node_modules/zone.js/dist/zone.js:387:0 <- test-config/karma-test-shim.js:123738:32)
	    at Zone.run (webpack:///node_modules/zone.js/dist/zone.js:138:0 <- test-config/karma-test-shim.js:123489:43)
	    at runInTestZone (webpack:///node_modules/zone.js/dist/jasmine-patch.js:145:0 <- test-config/karma-test-shim.js:126987:34)
	    at UserContext.<anonymous> (webpack:///node_modules/zone.js/dist/jasmine-patch.js:160:0 <- test-config/karma-test-shim.js:127002:20)
	    at <Jasmine>
	    at ZoneDelegate.invokeTask (webpack:///node_modules/zone.js/dist/zone.js:421:0 <- test-config/karma-test-shim.js:123772:31)
	    at Zone.runTask (webpack:///node_modules/zone.js/dist/zone.js:188:0 <- test-config/karma-test-shim.js:123539:47)
	    at drainMicroTaskQueue (webpack:///node_modules/zone.js/dist/zone.js:595:0 <- test-config/karma-test-shim.js:123946:35)
	    at run (webpack:///node_modules/core-js/modules/es6.promise.js:75:0 <- test-config/karma-test-shim.js:122284:22)
	    at webpack:///node_modules/core-js/modules/es6.promise.js:92:0 <- test-config/karma-test-shim.js:122301:30
	    at MutationObserver.flush (webpack:///node_modules/core-js/modules/_microtask.js:18:0 <- test-config/karma-test-shim.js:122519:9)
	Error: Expected no open requests, found 1: GET http://demo.alignak.net:5000/host
	    at HttpClientTestingBackend.verify (webpack:///node_modules/@angular/common/esm5/http/testing.js:477:0 <- test-config/karma-test-shim.js:138995:19)
	    at UserContext.<anonymous> (webpack:///src/backend/client.service.spec.ts:27:13 <- test-config/karma-test-shim.js:138450:18)
	    at ZoneDelegate.invoke (webpack:///node_modules/zone.js/dist/zone.js:388:0 <- test-config/karma-test-shim.js:123739:26)
	    at ProxyZoneSpec.onInvoke (webpack:///node_modules/zone.js/dist/proxy.js:128:0 <- test-config/karma-test-shim.js:126726:39)
	    at ZoneDelegate.invoke (webpack:///node_modules/zone.js/dist/zone.js:387:0 <- test-config/karma-test-shim.js:123738:32)
	    at Zone.run (webpack:///node_modules/zone.js/dist/zone.js:138:0 <- test-config/karma-test-shim.js:123489:43)
	    at runInTestZone (webpack:///node_modules/zone.js/dist/jasmine-patch.js:145:0 <- test-config/karma-test-shim.js:126987:34)
	    at UserContext.<anonymous> (webpack:///node_modules/zone.js/dist/jasmine-patch.js:160:0 <- test-config/karma-test-shim.js:127002:20)
	    at <Jasmine>
	    at ZoneDelegate.invokeTask (webpack:///node_modules/zone.js/dist/zone.js:421:0 <- test-config/karma-test-shim.js:123772:31)
	    at Zone.runTask (webpack:///node_modules/zone.js/dist/zone.js:188:0 <- test-config/karma-test-shim.js:123539:47)
	    at drainMicroTaskQueue (webpack:///node_modules/zone.js/dist/zone.js:595:0 <- test-config/karma-test-shim.js:123946:35)
	    at run (webpack:///node_modules/core-js/modules/es6.promise.js:75:0 <- test-config/karma-test-shim.js:122284:22)
	    at webpack:///node_modules/core-js/modules/es6.promise.js:92:0 <- test-config/karma-test-shim.js:122301:30
	    at MutationObserver.flush (webpack:///node_modules/core-js/modules/_microtask.js:18:0 <- test-config/karma-test-shim.js:122519:9)

I am able to proceed. This is not a bug. I had similar issue "Match by function: ", found none. It is solved after i added empty handler for then() in-case the method returns a Promise and subscribe() in-case method returns a Observable. .then( (data) => { console.log('success'); }

.subscribe( () => { console.log('success'); }