Quantcast
Channel: mattfrear – Matt's work blog
Viewing all articles
Browse latest Browse all 53

Stub typed HttpClients in ASP.NET 6 integration tests

$
0
0

For the last 8 years or so my work doesn’t have many unit tests in it, instead I favour integration tests which fire up and run the web API we are testing in memory, and then run tests against that. This way you test the entire stack as a black box, which has loads of benefits:

  • you’re testing the entire stack so all the Httphandlers etc
  • you can usually refactor the code freely, as your tests are not tied to the implementation

Years ago on .NET Framework we had to jump through all sorts of OWIN hurdles to get this to work, but fortunately ASP.NET Core has supported it since the beginning, and has decent documentation over at https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests – although, the documentation of late seems to be targeted more at ASP.NET Razor Pages rather than at APIs like it was a few years ago.

In the early days we went to the hassle of writing full on BDD tests using Specflow, but for most teams that’s too much effort and unless the BAs or Product Owners can actually access your source code and have an interest in reading your tests, then that’s usually wasted effort, so XUnit tests with Given When Then comments (or Arrange Act Assert) is usually what I roll with these days.

Integration tests shouldn’t make any external calls, so this means you need to come up with a strategy for faking the database and stubbing HTTP calls to 3rd parties. The latter is the topic of this post.

Lately I’ve been using typed Httpclients in my solutions. I couldn’t find it documented anywhere a way to stub typed HttpClients, so this is the solution I came up with.

I override the HttpClientFactory to always return the same mocked HttpClient.
Tests can call the StubHttpRequest method to stub any http request.

public class TestWebApplicationFactory<TStartup>
        : WebApplicationFactory<TStartup> where TStartup : class
{
    private readonly Mock<HttpMessageHandler> mockHttpMessageHandler;
    private Mock<IHttpClientFactory> mockFactory;

    public TestWebApplicationFactory()
    {
        mockHttpMessageHandler = new Mock<HttpMessageHandler>();
        var client = new HttpClient(mockHttpMessageHandler.Object);
        client.BaseAddress = new Uri("https://test.com/");
        mockFactory = new Mock<IHttpClientFactory>();
        mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(client);
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(sp =>
        {
            sp.AddScoped(sp => mockFactory.Object);
        });
    }

    public void StubHttpRequest<T>(string requestUrl, HttpStatusCode statusCode, T content)
    {
        _mockHttpMessageHandler
            .Protected()
            .Setup<Task<HttpResponseMessage>>("SendAsync",
                ItExpr.Is<HttpRequestMessage>(msg => msg.RequestUri!.ToString().EndsWith(requestUrl, StringComparison.InvariantCultureIgnoreCase)),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage
            {
                StatusCode = statusCode,
                Content = new StringContent(JsonConvert.SerializeObject(content)),
            });
    }
}

Then, the test code that uses it might look like:

public class GetAccountMembershipFeature : IClassFixture<TestWebApplicationFactory<Program>>
{
    private const string URL = "customers/{0}/accounts/{1}/membership";
    private readonly TestWebApplicationFactory<Program> _factory;
    private readonly HttpClient _sut;

    public GetAccountMembershipFeature(TestWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _sut = factory.CreateClient();
    }

    [Fact]
    public async Task GetAccountMembership_IsRegistered()
    {
        // GIVEN this customer and account
        long customerId = 88888888;
        long accountId = 9828282828;

        // WHEN the downstream system contains this membership information
        var membership = new AccountMembershipByTypeResponse
        {
            AccountNo = accountId,
            Amount = null,
            EndDate = null,
            ExternalReference = "7",
            MembershipType = membershipType,
            StartDate = new DateTime(2021, 1, 1)
        };

        _factory.StubHttpRequest(ApiUrls.AccountMembershipByTypeUrl(accountId.ToString(), membershipType, true), 
            HttpStatusCode.OK, 
            new List<AccountMembershipByTypeResponse> { membership });

        // THEN our GET endpoint should return the correct Amount
        var response = await _sut.GetFromJsonAsync<MembershipDetails>(string.Format(URL, customerId, accountId));
        response.ShouldNotBeNull();
        response.Amount.ShouldBe("7");
        response.Registered.ShouldBe(true);
    }

There’s also the ApiUrls helper which is a public method in the production code, which the test code also calls:

public static class ApiUrls
{
    public static string RegisterBankTransferUrl => "api/api/payment/transaction/BankTransfer";
    public static string CreateDirectDebitUrl => "api/payment/directdebit/create";
    public static string CreateSmoothPayUrl => "api/payment/smoothpay/create";

    public static string AccountMembershipByTypeUrl(string accountNo, string membershipType, bool currentOnly) =>
        $"api/accounts/{accountNo}/membershipbytype/{membershipType}?currentOnly={currentOnly}";

Hopefully that helps someone.


Viewing all articles
Browse latest Browse all 53

Trending Articles