skip to Main Content

Is there are way to verify the request object passed to HttpClient.PostAsJsonAsync. The request object is constructed inside the method to be unit tested. Therefore I need to verify that the request object is constructed with correct values.

I tried the following and I am expecting to verify the searchrequest object by comparing the values as shown below

public async Task Test()
{
    //Arrange
    var handlerMock = new Mock<HttpClientHandler>();
    handlerMock.Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.IsAny<HttpRequestMessage>(),
            ItExpr.IsAny<CancellationToken>()
        )
        .ReturnsAsync(new HttpResponseMessage
        {
            StatusCode = HttpStatusCode.OK,
            Content = new StringContent("{'foo':'bar'}")
        });
    httpClient = new HttpClient(handlerMock.Object);

    //ACT
    var criteria = new Criteria();
    await SomeMethodToTestAsync(criteria);

    //Assert
    var publishedDateStart = new DateOnly(2021, 10, 17);
    handlerMock.Protected().Verify(
        "SendAsync",
        Times.Exactly(1), // we expected a single external request
        ItExpr.Is<HttpRequestMessage>(req =>
            req.Method == HttpMethod.Post  // we expected a POST request
        ),
        ItExpr.IsAny<CancellationToken>(),
        ItExpr.Is<SearchRequest>(r => r.PublishedDateStart == publishedDateStart)   // this doesn't work
  );
}

To be tested method

public async Task SomeMethodToTestAsync(Criteria criteria)
{
    var url = "https://example.com";
    // build some complex request from criteria
    var searchRequest = new SearchRequest
    {
        PublishedDateStart = new DateOnly(2021, 10, 17)
    };
    var searchResponse = await httpClient.PostAsJsonAsync(url, searchRequest);

    //other code
}

2

Answers


  1. The PostAsJsonAsync extension method doesn’t pass a SearchRequest to SendAsync. It serializes the SearchRequest to JSON and packages that string in some kind of HttpContent object. This means that when verifying the SendAsync call, the test must inspect the HttpRequestMessage that the PostAsJsonAsync method generated.

    While really not recommended you can achieve that goal by changing the Verify call to something like this:

    handlerMock.Protected().Verify(
        "SendAsync",
        Times.Exactly(1), // we expected a single external request
        ItExpr.Is<HttpRequestMessage>(req =>
            req.Method == HttpMethod.Post && // we expected a POST request
            req.Content.ReadAsStringAsync().Result.Contains("2021-10-17")
        ),
        ItExpr.IsAny<CancellationToken>());
    

    While this passes the test, it’s really not recommended:

    • ItExpr.Is predicates need to run synchronously, which means that you need to use Result to pull the JSON string out of the Content. That’s not recommended, as it may cause deadlocks.
    • Such a test is fragile, because it depends heavily on implementation details beyond your control.

    If the implementation of PostAsJsonAsync or the internal workings of HttpClient changes when you update dependencies, this could easily break tests like this one. You’d end up in a frustrating game of Library Whac-A-Mole.

    Fragile Tests are generally associated with interaction-based tests. Consider state-based testing instead. Concretely, instead of using Moq at all, you may consider implementing a test-specific Fake HttpClientHandler.

    internal sealed class FakeHttpClientHandler : HttpClientHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            // Implement enough logic here to simulate the service that the
            // client will interact with.
        }
    }
    

    Another option is to stand up a self-hosted service and test against that.

    Login or Signup to reply.
  2. Making an assessment against the request body is a bit clumsy IMHO, because you can do that inside the ReturnsAsync.

    //Arrange
    var data = new { A = 1, B = "C"};
    
    var handlerMock = new Mock<HttpClientHandler>();
    handlerMock.Protected()
        .Setup<Task<HttpResponseMessage>>(
        "SendAsync",
        ItExpr.IsAny<HttpRequestMessage>(),
        ItExpr.IsAny<CancellationToken>()
    )
        .ReturnsAsync((HttpRequestMessage req, CancellationToken ct) => {
            //Assert
            var content = (JsonContent)req.Content;
            (content.Value == data).Dump();
    
            //Returns
            return new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.OK,
                Content = new StringContent("{'foo':'bar'}")
            };
        });
    
    //Act
    var httpClient = new HttpClient(handlerMock.Object);
    await httpClient.PostAsJsonAsync("http://google.com", data);
    
    • ReturnsAsync has an overload which can be used to dynamically compute the return value
      • This overload receives the mocked method’s parameter(s)
      • In this particular case, an HttpRequestMessage and a CancellationToken
    • The PostAsJsonAsync uses a JsonContent to represent the request body
      • The JsonContent has a Value property which allows you to access the original (not the serialized) object

    So, here (inside the ReturnsAsync) you can perform your assessment regarding to the request body without the need to do string comparison.

    Dotnet fiddle: https://dotnetfiddle.net/FYiqAk

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search