integration-testing.net-6.0aspnetcore-environment

How to do integration testing for .NET 6 Web Api projects using WebApplicationFactory?


I have been using WebApplicationFactory for integration testing in .NET 5, .NET Core 3.1 and .NET Core 2.1 . It is all working fine for me.

I just create my project with:

dotnet new webapi

and then I see a web api project with a Startup class.

Then I create an instance of the factory class:

private readonly WebApplicationFactory<Startup> factory = new WebApplicationFactory<Startup>();

As explained here and here.

The WebApplicationFactory class is something I really like. It makes it easy to write integration tests. I like it a lot. The Microsoft docs also refer to it for .NET 6.

However, how do I, when using .NET 6, make sure my code compiles?

The mentioned command-line statement:

dotnet new webapi

results in a nicely working project but (when having the .NET 6 SDK installed) without a Startup class...... Now my tests do not compile for just one reason: the Startup class does not exist.

How to fix this? I find it hard to imagine that I cannot write integration tests anymore because WebApplicationFactory<Startup> does not compile anymore. But right now, this is the reality I need to deal with. How can I solve the problem? I look forward to being able to use the WebApplicationFactory class and continue writing integration tests. But how?


Solution

  • Support for this was added in .NET 6 RC1. This is the first version to be supported in production.

    You can pass Program instead of Startup. Startup essentially merged into Program now. The Program class is automagically generated from the top-level statements in the Program.cs file. It's not a public class though, so you need to add InternalsVisibleTo to the application's project file to make it visible to the test project.

    <ItemGroup>
      <InternalsVisibleTo Include="MinimalApiPlayground.Tests" />
    </ItemGroup>
    

    In this example Damien Edwards defines a WebApplicationFactory<Program> to demonstrate testing a Todo minimal API.

    Creating a test app is essentially the same as using Startup : :

    [Fact]
    public async Task GetSwaggerUI_Returns_OK()
    {
        await using var application = new TodoApplication();
    
        var client = application.CreateClient();
        var response = await client.GetAsync("/swagger/index.html");
    
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
    
    class TodoApplication : WebApplicationFactory<Program>
    {
        protected override IHost CreateHost(IHostBuilder builder)
        {
            // Add mock/test services to the builder here
            builder.ConfigureServices(services =>
            {
                services.AddScoped(sp =>
                {
                    // Replace SQL Lite with test DB
                    return new SqliteConnection("Data Source=testtodos.db");
                });
            });
    
            return base.CreateHost(builder);
        }
    }
    

    The Program class in that example defines quite a lot of endpoints :

    
    using System.ComponentModel.DataAnnotations;
    using Microsoft.Data.Sqlite;
    using Dapper;
    
    var builder = WebApplication.CreateBuilder(args);
    
    var connectionString = builder.Configuration.GetConnectionString("TodoDb") ?? "Data Source=todos.db";
    builder.Services.AddScoped(_ => new SqliteConnection(connectionString));
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    var app = builder.Build();
    
    await EnsureDb(app.Services, app.Logger);
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error");
    }
    
    app.MapGet("/error", () => Results.Problem("An error occurred.", statusCode: 500))
       .ExcludeFromDescription();
    
    app.MapSwagger();
    app.UseSwaggerUI();
    
    app.MapGet("/", () => "Hello World!")
       .WithName("Hello");
    ...
    app.MapDelete("/todos/delete-all", async (SqliteConnection db) => Results.Ok(await db.ExecuteAsync("DELETE FROM Todos")))
        .WithName("DeleteAll")
        .Produces<int>(StatusCodes.Status200OK);
    
    app.Run();