Search code examples
c#.netintegration-testingplaywright

Writing Integration tests with WebApplicationFactory and Playwright in .Net 6/7


I've been stuck with this issue the whole day and I can't find a solution. What I am trying to solve is exactly the same problem that is supposedly solved in this Article.

It is also related to this issue with a very similar solution. Although in this issue they are using Selenium.

The solution is basically overriding the CreateHost method from WebApplicationfactory<Program> as demonstrated here:

protected override IHost CreateHost(IHostBuilder builder)  
{  
    // Create the host for TestServer now before we  
    // modify the builder to use Kestrel instead.    
    var testHost = builder.Build();  
 
    // Modify the host builder to use Kestrel instead  
    // of TestServer so we can listen on a real address.    
 
    builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel());  
 
    // Create and start the Kestrel server before the test server,  
    // otherwise due to the way the deferred host builder works    
    // for minimal hosting, the server will not get "initialized    
    // enough" for the address it is listening on to be available.    
    // See https://github.com/dotnet/aspnetcore/issues/33846.    
 
    _host = builder.Build();  
    _host.Start();  
 
    // Extract the selected dynamic port out of the Kestrel server  
    // and assign it onto the client options for convenience so it    
    // "just works" as otherwise it'll be the default http://localhost    
    // URL, which won't route to the Kestrel-hosted HTTP server.     
 
    var server = _host.Services.GetRequiredService<IServer>();  
    var addresses = server.Features.Get<IServerAddressesFeature>();  
 
    ClientOptions.BaseAddress = addresses!.Addresses  
        .Select(x => new Uri(x))  
        .Last();  
 
    // Return the host that uses TestServer, rather than the real one.  
    // Otherwise the internals will complain about the host's server    
    // not being an instance of the concrete type TestServer.    
    // See https://github.com/dotnet/aspnetcore/pull/34702.   
 
    testHost.Start();  
    return testHost;  
}

I am however, getting an error “Build can only be called once” at the moment I call the Build() method in the IHostBuilder. Other people also have commented in the blog post that they got this error but no response was found.

I read through the comments and documentation online and tried to modify for the following:

protected override IHost CreateHost(IHostBuilder builder)
{
    // Create the host for TestServer now before we
    // modify the builder to use Kestrel instead.
    var testHost = builder.Build();

    // Modify the host builder to use Kestrel instead
    // of TestServer so we can listen on a real address.
    //builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel());

    // Create and start the Kestrel server before the test server,
    // otherwise due to the way the deferred host builder works
    // for minimal hosting, the server will not get "initialized
    // enough" for the address it is listening on to be available.
    // See https://github.com/dotnet/aspnetcore/issues/33846.
    Host = Program.CreateHostBuilder(null)
                  .ConfigureWebHost(ConfigureWebHost)
                  .Build();

    Host.Start();

    // Extract the selected dynamic port out of the Kestrel server
    // and assign it onto the client options for convenience so it
    // "just works" as otherwise it'll be the default http://localhost
    // URL, which won't route to the Kestrel-hosted HTTP server.
    var server = Host.Services.GetRequiredService<Microsoft.AspNetCore.Hosting.Server.IServer>();
    var addresses = server.Features.Get<IServerAddressesFeature>();

    ClientOptions.BaseAddress = addresses!.Addresses
        .Select(x => new Uri(x))
        .Last();

    // Return the host that uses TestServer, rather than the real one.
    // Otherwise the internals will complain about the host's server
    // not being an instance of the concrete type TestServer.
    // See https://github.com/dotnet/aspnetcore/pull/34702.
    testHost.Start();
    return testHost;
}

Notice I created the IHost directly from the Program class.

Now when I run the following lines in my test:

var playwright = await Playwright.CreateAsync();

var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = false, SlowMo = 50 });

var page = await browser.NewPageAsync();

await page.GotoAsync(webApplicationSuitFixture.BaseUrl);

I can actually see that Playwright opens a Chrome page (Which would not happen without the CreateHost override above). However the application is not running this way, it gives me a Fatal page without much more to go on.

Obs: The blazor app runs normally when I just start it. It is currently on .Net 6. The test project is currently in .Net 7. I tried to switch the .Net versions and update the packages accordingly, but the same problem persists.

Any help is appreciated!


Solution

  • After more testing, I figure out that the problem was actually on the target project of the tests.

    The project was created prior to .Net 6 and then migrated, therefore it still had the "Old" Initialization from Host:

    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            // Code here...
        }
    

    This will not work. Instead, the IHostBuilder is replaced by the new WebApplication

    var builder = WebApplication.CreateBuilder(args);
    

    This will allow the call of the Build() to be performed more than once. And the solution proposed by Daniel Donbavand will work.