Skip to content

Decouple WebApplicationFactory from TestServer implementation #60370

Closed as not planned
@mkArtakMSFT

Description

@mkArtakMSFT

Background and Motivation

We have a long-standing popular issue tracking improvement of automated browser testing with real server. Part of the ask is to decouple the WebApplicationFactory from the TestServer implementation, as they're currently [tightly coupled] (

).

Unfortunately, the TestServer is also exposed in WebApplicationFactory via a public property Server. Hence, decoupling requires an API change proposed below.

The second change is exposing the initialization logic publicly. Without this change, customers had to call the CreateClient() API which would internally initialize the server but wouldn't use the returned HttpClient instance. This will make it more intuitive and avoid creating unnecessary objects. Here is a screenshot from a blogpost pointing out this odd usage pattern:

Image

Another example of not-so-great solution that the community came up with can be found here:

Image

Here, the developer calls the CreateDefaultClient() in a derived class to force initialization.

With the proposed changes, customers can now simply call server.Initialize() instead which is more intuitive.

Proposed API

New abstraction / interface to depend on instead of the TestServer implementation.

namespace Microsoft.AspNetCore.TestHost;

public interface ITestServer : IServer
{
    IWebHost Host { get; }
    HttpMessageHandler CreateHandler();
    HttpClient CreateClient();
}

- public class TestServer : IServer
+ public class TestServer : ITestServer

Usage / API change in WebApplicationFactory:

namespace Microsoft.AspNetCore.Mvc.Testing;

public partial class WebApplicationFactory<TEntryPoint> : IDisposable, IAsyncDisposable where TEntryPoint : class
{
    [Obsolete("This property is obsolete. Consider utilizing the TestServer property instead.")]
    public TestServer? Server { get; }
   +public ITestServer? TestServer { get; }

   - private void EnsureServer()
   + public void Initialize()


    [Obsolete("This method is obsolete. Consider utilizing the CreateTestServer method instead.")]
    protected virtual TestServer CreateServer(IWebHostBuilder builder) => new TestServer(builder);
    + protected virtual ITestServer CreateTestServer(IWebHostBuilder builder) => CreateServer(builder);
}

Usage Examples

With this change, developers will be able to avoid creating artificial server instances only to fulfil the contract. Instead, here is an example of how a developer will override the newly added CreateTestServer method, that will utilize the very same server created by the host and use an adapter to utilize it as ITestServer:

protected override ITestServer CreateTestServer(IWebHostBuilder builder)
{
    var webHost = builder.Build();
    webHost.Start();

    var server = webHost.Services.GetRequiredService<IServer>();
    RootUri = new Uri(server.Features.Get<IServerAddressesFeature>().Addresses.LastOrDefault());
    ClientOptions.BaseAddress = RootUri;
    return new KestrelTestServerAdapter(server, webHost);
}

Note, that the KestrelTestServerAdapter type is something the developers will need to come up with. Here is a sample implementation:

Sample IServer adapter

Below is the adapter class to encapsulate real IServer implementation and expose it as ITestServer.
 

internal class KestrelTestServerAdapter : ITestServer
{
    private readonly IServer _server;
    private readonly IWebHost _host;

    public KestrelTestServerAdapter(IServer server, IWebHost webHost)
    {
        _server = server ?? throw new ArgumentNullException(nameof(server));
        _host = webHost ?? throw new ArgumentNullException(nameof(webHost));
    }

    public IWebHost Host => _host;

    public IFeatureCollection Features => _server.Features;

    public HttpClient CreateClient()
    {
        return _host.Services.GetService<HttpClient>();
    }

    public HttpMessageHandler CreateHandler()
    {
        return new HttpClientHandler();
    }

    public void Dispose()
    {
        this._server.Dispose();
        this._host.Dispose();
    }

    public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull
    {
        return _server.StartAsync<TContext>(application, cancellationToken);
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return _server.StopAsync(cancellationToken);
    }
}

Alternative Designs

Risks

Metadata

Metadata

Assignees

Labels

api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templates

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions