Simpler Alternative to WebApplicationFactory ("compose instead of inherit" / no `DeferredHost`)
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 281
Description
### Is there an existing issue for this?
- [X] I have searched the existing issues
## ** After some valuable feedback from @davidfowl I'm rephrasing this ticket. ** Further below you'll find the original issue report so that you can still follow how I end up with the request.
### Is your feature request related to a problem? Please describe the problem.
Features of `WebApplicationFactory` are not available when using `TestServer` directly.
Basically it would be nice to have features of `WebApplicationFactory` without it calling the application's entry point:
- HttpClient with
- cookie support (`CookieContainerHandler`)
- redirect support (`RedirectHandler`)
- [tracking](https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L31) and disposing of created `HttpClients`
- Setting up the content root (to point to the assembly/project of a type of my choice).
(maybe this is insensible because it's already easy, I haven't investigated this closely)
- .. other features I forgot...?
So basically I propose to move all the stuff from the `WebApplicationFactory` to `TestServer` except what relates to calling the application's entry point.
## Original Issue
### Is your feature request related to a problem? Please describe the problem.
I was trying to create a TestServer for a .net 7 Web App.
`WebApplicationFactory` "works" but it's complicated because it's basically running the entry point of the real application.
That means, for tests now I have to replace relevant parts to make sure they don't use the "real thing". Depending on how program.cs is written I'll have to fulfill dependencies (on config values, for example), even though they are never relevant for the the test system (apart by reason of being referenced in program.cs - but later being replaced).
I don't think this ever was good practice, so I'll refrain from arguing this further at the moment (if you ask me to, I will).
This somewhat relates to https://github.com/dotnet/aspnetcore/issues/33846, because a conclusion there is as well that the implementation of Microsoft.AspNetCore.Mvc.Testing should be split into (externally) reusable parts.
It also relates to issues like https://github.com/dotnet/aspnetcore/issues/38335, because having a simple alternative to `WebApplicationFactory` would obviate the need for `DeferredHostBuilder` and thus get rid of the problems it brings.
### Describe the solution you'd like
After a few hours of reading the source code of Microsoft.AspNetCore.Mvc.Testing, debugging it and trial & error I came up with the following (for xUnit).
It contains a bit of source code copied from Microsoft.AspNetCore.Mvc.Testing (which I would like to prevent).
This is how it looks:
### Program.cs
var builder = WebApplication.CreateBuilder(args);
var config = /* reading the config */;
ConfigureApplication( // this is reused in tests
builder,
config);
var app = builder.Build();
ConfigurePipeline(app);
app.Run();
## xUnit Fixture
### Base Class
This is basically what should be provided by Microsoft.AspNetCore.Mvc.Testing. Of course it would be fine if it would not implement xUnit's `IAsyncLifetime` interface. Also, currently this is missing a few of the features which are part of Microsoft.AspNetCore.Mvc.Testing.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Mvc.Testing.Handlers;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace WebApiServer.SystemTests;
public abstract class AbstractTestServerFixture :
IAsyncLifetime
{
private readonly CancellationTokenSource cancellationTokenSource;
private readonly List clients;
private WebApplication? app;
private TestServer? server;
protected AbstractTestServerFixture()
{
cancellationTokenSource = new();
clients = new();
}
async Task IAsyncLifetime.InitializeAsync()
{
var builder = WebApplication.CreateBuilder(
new WebApplicationOptions
{
EnvironmentName = Environments.Development
});
builder.WebHost
.UseShutdownTimeout(TimeSpan.FromSeconds(5))
.UseTestServer();
await ConfigureApplication(builder);
app = builder.Build();
await ConfigurePipeline(app);
server = (TestServer)app.Services.GetRequiredService();
await app.StartAsync(cancellationTokenSource.Token);
}
protected abstract Task ConfigureApplication(WebApplicationBuilder builder);
protected abstract Task ConfigurePipeline(WebApplication app);
async Task IAsyncLifetime.DisposeAsync()
{
foreach (var client in clients)
{
client.Dispose();
}
cancellationTokenSource.Cancel();
app?.StopAsync(TimeSpan.FromSeconds(30));
await (app?.DisposeAsync() ?? ValueTask.CompletedTask);
server?.Dispose();
}
public HttpClient CreateClient()
{
return CreateClientInternal(
new RedirectHandler(7),
new CookieContainerHandler());
}
// MIT License
// loosely based on the source code of https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing
// found at https://github.com/dotnet/aspnetcore/tree/main/src/Mvc/Mvc.Testing/src
// (version at the time of writing: https://github.com/dotnet/aspnetcore/tree/cbb4916ecc63785267a750a78b42f3a769230509/src/Mvc/Mvc.Testing/src)
private HttpClient CreateClientInternal(
params DelegatingHandler[] handlers)
{
HttpClient client;
if (handlers.Length == 0)
{
client = server!.CreateClient();
}
else
{
for (var i = handlers.Length - 1; i > 0; i--)
{
handlers[i - 1].InnerHandler = handlers[i];
}
var serverHandler = server!.CreateHandler();
handlers[^1].InnerHandler = serverHandler;
client = new HttpClient(handlers[0]);
}
clients.Add(client);
client.BaseAddress = server.BaseAddress;
return client;
}
}
### Concrete Fixture Class
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Xunit.Abstractions;
namespace WebApiServer.SystemTests;
public class WebApiServerFixture :
AbstractTestServerFixture
{
protected override Task ConfigureApplication(
WebApplicationBuilder builder)
{
var config = /* your test config */;
Program.ConfigureApplication(
builder,
config);
return Task.CompletedTask;
}
protected override Task ConfigurePipeline(
WebApplication app)
{
Program.ConfigurePipeline(app);
return Task.CompletedTask;
}
}
## Test
namespace WebApiServer.SystemTests;
public class OAuthTests :
IClassFixture
{
private readonly WebApiServerFixture server;
public OAuthTests(WebApiServerFixture server)
{
this.server = server;
}
[Fact]
public async Task SomeTest()
{
var client = server.CreateClient();
var response = await client.GetAsync("not/an/existing/route");
response.EnsureSuccessStatusCode(); // Status Code 200-299
}
}
### Additional context
_No response_
Contributor guide
Research direction
Start by reading src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs and the TestServer-related entry points mentioned in the issue. Define which WebApplicationFactory features can be reused without invoking the application's entry point, then verify that the resulting API covers client handling, content-root setup, lifecycle, and disposal requirements.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100