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
}
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.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.