richardszalay/mockhttp

GetMatchCount does not recognize outbound requests made with the same base address

bensmith009988 opened this issue · 3 comments

I apologize for the wordy title, here is what I mean:

Consider your MUT (method under test) has 2 outbound requests. The full URL of the request looks as follows:

  1. https://testendpoint/test/jobposts?site=temp&locale=en-us
  2. https://testendpoint/test/jobposts?site=temp&locale=en-us&limit=3

This scenario is common with programmatic paging requests against a 3rd party API, where subsequent requests will include a query parameter differentiating how much data to pull back, or the page for which to request, etc.

After mocking the requests, and executing the code (which works as expected), using the GetMatchCount in an assertion statement of a test yields that the first request was matched twice, and the second request was matched 0 times. Let me provide some code snippets for you to see what I mean:

Test Method:

public async Task GetInternalJobPostings()
{
    var pageSize = 3;
 
    HttpResponseMessage? firstResponse = await client.GetAsync($"https://testendpoint/test/jobposts?site=internal&locale=en-us&limit=3");

    var responseString = await firstPageResponse.Content.ReadAsStringAsync();
    var firstPageJobs = JsonConvert.DeserializeObject<JobsResponse>(responseString);
    if (firstPageJobs.Total > firstPageJobs.Limit) // "enter this block if there is additional data that needs to be grabbed in another request"
    {
        for (var offset = pageSize; offset < firstPageJobs.Total; offset += pageSize) // "irrelevant logic, this is just the basis of the paging logic in this example. The point is that is makes another request"
        {
            HttpResponseMessage? nextPageResponse = await client.GetAsync($"https://testendpoint/test/jobposts?site=internal&locale=en-us&limit=3&offset={offset}");
            
            // Do some processing on the response here
        }
    }

    return;
}

Fake xUnit unit test:
I understand I have not defined data objects for you to see, nor do I provide the class builders and such. For the sake of understanding this code, imagine that the data response object that is not present returns data that only supports the GetInternalJobPostings method to make 2 requests total, only going through the paging logic once.

[Fact]
public async Task SomeTestMethod()
{
    // Arrange
    var builder = new JobsBuilder();
    var mockHttp = new MockHttpMessageHandler();

    var firstRequest = mockHttp.When(HttpMethod.Get, $"https://testendpoint/test/jobposts")
                               .WithQueryString(new Dictionary<string, string>()
                               {
                                   { "site", "internal"},
                                   { "locale", "en-us"},
                                   { "limit", "3"}
                               })
                               .Respond(req => new HttpResponseMessage(HttpStatusCode.OK)); // imagine this also returns a data object in the http response message that supports the paging logic

    var secondRequest = mockHttp.When(HttpMethod.Get, $"https://testendpoint/test/jobposts")
                               .WithQueryString(new Dictionary<string, string>()
                               {
                                   { "site", "internal"},
                                   { "locale", "en-us"},
                                   { "limit", "3"},
                                   { "offset", "3"}
                               })
                               .Respond(req => new HttpResponseMessage(HttpStatusCode.OK)); // imagine this also returns a data object in the http response message that supports the paging logic

    var client = mockHttp.ToHttpClient();

    var jobsProvider = builder.WithClient(client)
                                              .Build();
    // Act
    await jobsProvider.GetInternalJobPostings();

    // Assert
    mockHttp.GetMatchCount(firstRequest).Should().Be(1); // assertion statement using the FluidAssertions NuGet package
    mockHttp.GetMatchCount(secondRequest).Should().Be(1); // assertion statement using the FluidAssertions NuGet package
}

Running this test gives the error:
mockHttp.GetMatchCount(firstRequest) to be 1, but found 2

Additionally, remediating that error by changing the assertion to expect 2 requests on the first yields the following error:
mockHttp.GetMatchCount(secondRequest) to be 1, but found 0

As you can see, MockHttp method GetMatchCount cannot properly differentiate between requests that only differ by query parameters.

I want to clarify that this behavior is only seen when the mockHttp.When method is used to mock expectations. The mockHttp.Expect does not exhibit this behavior.

Answering from my phone so apologies if I've missed something. I'll try to actually run your example in the next few days.

On the surface it looks like your problem is that your first When could also apply to your second call so it's actually matching it twice.

To fix, do one of:

  1. Put your more specific When (with the offset key) before the original one
  2. Use WithExactQuerystring on your first When so it will not match against the offset key
  3. Use Expect instead of When, since you are expecting them in a specific order

As you can see, MockHttp method GetMatchCount cannot properly differentiate between requests that only differ by query parameters.

Also just to clarify, GetMatchCount doesn't do any evaluation of requests against mocked requests - it just reports on what was already matched. The issue here is that the first mocked request is being matched twice (which, as above, is due to the way WithQueryString works vs WithExactQueryString)

From the README

WithQueryString:

Matches on one or more querystring values, ignoring additional values

WithExactQueryString:

Matches on one or more querystring values, rejecting additional values