Override Host configuration in Integration Testing using ASP NET Core 6 Minimal APIs model
Asked Answered
N

2

7

Being able to customize the Host configuration in integration tests is useful to avoid calling services that shouldn't run at all during testing. This was easily achievable using the standard Startup model, overriding the CreateHostBuilder from the WebApplicationFactory. I tried many many things using the "Minimal APIs" approach and couldn't figure it out.

A full example of what was possible using the Startup model, would be something like this:

Program.cs:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
            // This is a service that downloads Configuration from the internet, to be used
            // as a source for `IConfiguration`, just like any `appsettings.json`.
            // I don't want this running during testing
            .AddConfigServer();
}

As you can imagine, the AddConfigServer calls an external web server to download the configuration I want to use in my app startup, but I definitely don't want my integration tests calling this external web server for several reasons: don't want to depend on external services, don't want to hammer my config server with testing requests, don't want my server customizations to be exposed to my tests, etc.

Using the Startup model, I could easily change this behavior with this:

public class MyWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override IHostBuilder CreateHostBuilder() =>
        Host.CreateDefaultBuilder()
            // No AddConfigServer for my integration tests
            .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
    }

    //... other customizations
}

However, trying this same approach throws an exception on my integration test startup:

System.InvalidOperationException : No application configured. Please specify an application via IWebHostBuilder.UseStartup, IWebHostBuilder.Configure, or specifying the startup assembly via StartupAssemblyKey in the web host configuration.

Since I don't have a Startup class, I couldn't figure out how to properly do this. In fact, since this is common in all my web projects, I abandoned Minimal APIs altogether for the moment, until I figure out how to properly achieve this without a Startup class.

PS.: I have all the "partial Program" and "InternalsVisibleTo" in place, as described here.

Narcoanalysis answered 19/6, 2022 at 17:57 Comment(2)
I think you could refer to this case:#68987557Barragan
That's not exactly what I need, I am already using the WebApplicationFactory as mentioned in that answer, what I need to do is override the HostBuilder altogether. I don't want to replace or add new services to it, I want to start fresh with no services registered in my tests.Narcoanalysis
S
24

In order to make this work, you have to provide your configuration values twice both before and after the minimal API Program.cs runs.

Here's how I did it:

public class MyFixture : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        var configurationValues = new Dictionary<string, string>
        {
            { "MyConfigSetting", "Value" }
        };
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(configurationValues)
            .Build();

        builder
            // This configuration is used during the creation of the application
            // (e.g. BEFORE WebApplication.CreateBuilder(args) is called in Program.cs).
            .UseConfiguration(configuration)
            .ConfigureAppConfiguration(configurationBuilder =>
            {
                // This overrides configuration settings that were added as part 
                // of building the Host (e.g. calling WebApplication.CreateBuilder(args)).
                configurationBuilder.AddInMemoryCollection(configurationValues);
            });
    }
}
Sucker answered 24/6, 2022 at 18:33 Comment(1)
This is genuinely golden. It's not really well documented and it solves the issue of changing configurations used to dynamically register services at host-building time (if (host.Configuration.GetValue<bool>(...))) for tests.Cide
D
0

A similar solution that was tested in .NET 8.0 and uses service descriptors to register services into service collection via WebApplicationFactory:

    public class InMemoryApi : WebApplicationFactory<Program>
    {
        public HttpClient Client => _client;

        private List<(Type contract, object implementation)> _serviceDescriptors = new List<(Type contract, object implementation)>();

        public InMemoryApi()
        {
            // This code will replace IOrderRepository with TestOrderRepo instance for integration tests
            var testRepo = new TestOrderRepo();
            ReplaceService<IOrderRepository>(testRepo);

            _client = CreateClient(testRepo);
        }

        private void ReplaceServices(IServiceCollection services)
        {
            foreach (var service in _serviceDescriptors)
            {
                ServiceDescriptor serviceDescriptor = ServiceDescriptor.Transient(service.contract, x => service.implementation);
                services.Replace(serviceDescriptor);
            }
        }

        public void ReplaceService<T>(object serviceInstance)
        {
            _serviceDescriptors.Add((typeof(T), serviceInstance));
        }

        protected override IHost CreateHost(IHostBuilder builder)
        {
            builder.ConfigureServices(ReplaceServices);

            return base.CreateHost(builder);
        }
    }
Dairen answered 7/5 at 14:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.