ScottishTapWater
ScottishTapWater

Reputation: 4846

Inject HttpClient from WebApplicationFactory

I'm trying to create a (mostly) unified set of integation tests that can either be targetted at an in-memory API created from a WebApplicationFactory or at a fully-deployed version of our app. Using XUnit.DependencyInjection, I'm planning on injecting a HttpClient into my tests that either points to the test server or the real app based on an environment variable.

So to create a client for the test server, I can just run the following in Startup.cs:

WebApplicationFactory<Program> app = new();
HttpClient client = app.CreateClient();

This seems to work. However, I have absolutely no idea how to inject this implementation for the HttpClient into the individual test classes.

Something like this, doesn't work (such an overload doesn't exist):

services.AddHttpClient<MyTestClass>(client);

And neither does this (the injected client has the BaseAddress set to null for some reason):

services.AddHttpClient<InMemoryServerSelfTests>(c =>
                                    {
                                        c.BaseAddress           = client.BaseAddress;
                                        c.Timeout               = client.Timeout;
                                    });

My only other thought is to create a new class that wraps both clients and inject that instead but that seems messy:

public class TestClientWrapper
{
    public readonly HttpClient Client;
    public TestClientWrapper(InMemoryTestServer server)
    {
        Client = server.CreateClient();
    }

    public TestClientWrapper(HttpClient client)
    {
        Client = client;
    }
}

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    string targetEndpoint = Environment.GetEnvironmentVariable("targetEndpoint"); // Make this configurable
    bool   isLocal        = string.IsNullOrEmpty(targetEndpoint);
    
    if (isLocal)
    {
        InMemoryTestServer app = new();
        services.AddSingleton(new TestClientWrapper(app));
    }
    else
    {
        HttpClient client = new();
        services.AddSingleton(new TestClientWrapper(client));
    }
}

So really, I'm a bit stumped... Any ideas on how to accomplish this?

Upvotes: 5

Views: 5274

Answers (3)

satnhak
satnhak

Reputation: 9871

The problem is that the HttpClient generated by the WebApplicationFactory is special as the WebApplicationFactory is hosted in memory and is not visible out of process (I think that's what I read elsewhere). What that means is that copying over the settings doesn't work.

The only way I've managed to get the WebApplicationFactory client registered so that it is resolvable is to register an instance of IHttpClientFactory with the container that returns clients from the WebApplicationFactory.

public class TestHttpClientFactory<TStartup> : IHttpClientFactory 
    where TStartup : class
{
    private readonly WebApplicationFactory<TStartup> _appFactory;

    public TestHttpClientFactory(WebApplicationFactory<TStartup> appFactory) => _appFactory = appFactory;

    public HttpClient CreateClient(string name) => _appFactory.CreateClient();
}
services.AddSingleton<IHttpClientFactory>(new TestClientFactory(...));

Something along those lines will work.

Upvotes: 7

Hassan Faghihi
Hassan Faghihi

Reputation: 2039

Different Scenario:

Problem:

Consider you have two Web/API Applications, and one of them should send requests to the other one, let say I have a FakeApi that I want to call from my Web application or you may have an API application that is getting called by a BFF (Backend for Frontend) application.

Test -> Main application -> FakeApi
Test -> BFF -> API

How we should tell the target app, to use the second app?

Solution:

For this, I made my code adapt to the solution that @satnhak provided, although the code had a few issues, so I updated that post as well (if the moderator approves). So the main code is the same, so give him the credit first.

Okay, in my case I have Gherkin/Specflow Test scenarios and test step definitions. I have two WebApplicationFactory wrapper per my Web Applications, called FakeApiWebApplicationFactory and MainWebApplicationFactory.

Considering I'm testing the application in MainWebApplicationFactory and it calls to the FakeApiWebApplicationFactory for extra services.

For this using the @satnhak code I create a HttpClientFactory, and pass it to the MainWebApplicationFactory which inside of it I have a service that inject the HttpClient, and the HttpClient is added using :

// Program.cs in Services project
// ---- Program CS should define class if you want to pass it to WebApplicationFactory

builder.Services.AddTransient<IMyService, MyService>();

builder.Services.AddHttpClient<IMyService, MyService>(client =>
{
    client.BaseAddress = new Uri(myServiceSetting.BaseApiUrl);
});

The MyService injects an HttpClient inside itself, and uses it to connect to the fake or real API.

public class MyService: IMyService
{
    private readonly HttpClient _httpClient;

    public GoCardlessBankAccountDataService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

So here are my web factories:

using FakeApi; // Program.cs in FakeApi project
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;

namespace AcceptanceTests;

public class FakeApiWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        // You can modify your configuration here if needed
        builder.ConfigureServices(services =>
        {
            // E.g., override some services for testing purposes
        });
    }
}
using Services; // Program.cs in Services project
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;

namespace AcceptanceTests;

public class MainWebApplicationFactory : WebApplicationFactory<Program>
{
    private readonly IHttpClientFactory? _fakeApiHttpClientFactory;

    public MainWebApplicationFactory(IHttpClientFactory? fakeApiHttpClientFactory)
    {
        _fakeApiHttpClientFactory = fakeApiHttpClientFactory;
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        if (_fakeApiHttpClientFactory is not null)
        {
            builder.ConfigureServices(services =>
            {
                // E.g., override some services for testing purposes
                services.AddSingleton(_fakeApiHttpClientFactory);
            });
        }
    }
}

In the above code as you can see put a condition, which I believe is necessary, since I may want to use real service in some of my tests, and I may want to override the HttpClientFactory in some others.

Now I wire these classes as following (Im using Specflow again which in turn it uses the XUnit) This is the begining of my test file:

private readonly ScenarioContext _scenarioContext;
    
    private readonly FakeApiWebApplicationFactory _fakeApiFactory;
    private readonly HttpClient _fakeApiClient;
    
    private readonly MainWebApplicationFactory _mainFactory;
    private readonly HttpClient _mainClient;

    public MainServiceIntegrationIntegrationStepDefinitions(ScenarioContext scenarioContext)
    {
        _scenarioContext = scenarioContext;
        _fakeApiFactory = new FakeApiWebApplicationFactory();
        _fakeApiClient = _fakeApiFactory.CreateClient();

        _mainFactory = new MainWebApplicationFactory(new TestHttpClientFactory<Program>(_fakeApiFactory));
        _mainServicesClient = _mainFactory.CreateClient();
    }

As you can see, I first create the FakeApi, then using @satnhak code, I pass this factory to the Main Service. Then the main application uses the factory to create an instance of HttpClient that is generated by by the FakeApiWebApiFactory.

Note: As I tested you don't need to care about the BaseUrl, as my application sets the BaseUrl as you already scene in the Problem section, but the in-memory HttpClient made by the WebApplicationFactory, still manage to send request to the FakeApi and overcome the customized base Url.

TestHttpClientFactory:

public class TestHttpClientFactory<TStartup> : IHttpClientFactory 
    where TStartup : class
{
    private readonly WebApplicationFactory<TStartup> _appFactory;

    public TestHttpClientFactory(WebApplicationFactory<TStartup> appFactory) => _appFactory = appFactory;

    public HttpClient CreateClient(string name) => _appFactory.CreateClient();
}

Upvotes: 2

berhir
berhir

Reputation: 1450

I created a custom HttpMessageHandlerBuilder to solve this issue.

public class TestServerHttpMessageHandlerBuilder : HttpMessageHandlerBuilder
{
    public TestServerHttpMessageHandlerBuilder(TestServer testServer, IServiceProvider services)
    {
        Services = services;
        PrimaryHandler = testServer.CreateHandler();
    }

    private string? _name;

    [DisallowNull]
    public override string? Name
    {
        get => _name;
        set
        {
            ArgumentNullException.ThrowIfNull(value);
            _name = value;
        }
    }

    public override HttpMessageHandler PrimaryHandler { get; set; }

    public override IList<DelegatingHandler> AdditionalHandlers { get; } = new List<DelegatingHandler>();

    public override IServiceProvider Services { get; }

    public override HttpMessageHandler Build()
    {
        if (PrimaryHandler == null)
        {
            throw new InvalidOperationException($"{nameof(PrimaryHandler)} must not be null");
        }

        return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers);
    }
}

Then in the startup code register the custom TestServerHttpMessageHandlerBuilder BEFORE calling AddHttpClient.

clientServices.AddTransient<HttpMessageHandlerBuilder>(sp => new TestServerHttpMessageHandlerBuilder(webAppFactory.Server, sp));

AddHttpClient checks if a HttpMessageHandlerBuilder was already registered and uses it instead of the default implementation. Now ALL HttpClients created using the HttpClientFactory will use the TestServer.

Upvotes: 2

Related Questions