skip to Main Content

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?

2

Answers


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

    class TestClientFactory : IHttpClientFactory
    {
        WebApplicationFactory<Startup> _appFactory;
    
        public TestClientFactory(TestClientFactory _appFactory) => this._appFactory= appFactory;
    
        public HttpClient CreateClient(string name) => this._appFactory.CreateClient();
    }
    
    services.AddSingleton<IHttpClientFactory>(new TestClientFactory(...));
    

    Something along those lines will work.

    Login or Signup to reply.
  2. 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.

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