How to write real integration test in .NET Core?

.net-core asp.net-core-webapi entity-framework-core integration-testing tdd

Question

I have two questions, but let me start from the beginning. I have ASP.NET Core 3.1 WebAPI and I'm looking for the most optimal way to end-to-end test my controllers (I don't want to use InMemory provider). I separated my tests in two sets: "Read-only" (GET) and "Read-write" (POST, PUT, PATCH, DELETE). For Read-only tests I want create brand new database (I use code-first migrations) once, and then tests all GET requests one by one. For Read-write requests, I want to create new database and drop it after the test.

This is what I've done so far:

public class TestFixture<TStartup> : IDisposable where TStartup : class
{
    private readonly TestServer _server;

    public TestFixture()
    {
        var builder = new WebHostBuilder().UseStartup<TStartup>();
        builder.ConfigureAppConfiguration((context, conf) =>
        {
            conf.AddUserSecrets(typeof(Startup).GetTypeInfo().Assembly);
        });

        _server = new TestServer(builder);

        Client = _server.CreateClient();
        Client.BaseAddress = new Uri("http://localhost:5000");
    }

    public HttpClient Client { get; }

    public void Dispose()
    {
        Client.Dispose();
        _server.Dispose();
    }
}

... and the test:

public class TestControllerShould : IClassFixture<TestFixture<Startup>>
{
    public HttpClient Client { get; }

    public TestControllerShould(TestFixture<Startup> fixture)
    {
        Client = fixture.Client;
    }

    [Fact]
    public async Task GetHelloWorld()
    {
        // Arrange
        var request = new HttpRequestMessage(new HttpMethod("GET"), "/test/");

        // Act
        var response = await Client.SendAsync(request);

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadAsStringAsync();
        Assert.Equal("Hello World!", content);
    }
}

I have two problems. First of all, I'm using Startup from main project (which is desired, because I want to test it after all) with production connection string, so that's one problem. Second problem, I would to create and drop database after each "write" test, which I can't from obvious reasons. So my questions are:

  1. How can I use Startup class, but only change the database name to "MyDatabaseName + Guid.NewGuid()"?
  2. Second question, how can I create and drop database (with unique name) before and after every test?

PS. I don't want to use InMemory provider. I also don't want to use transaction and rollback at the end of the test. I want to do real integration test.

1
1
4/9/2020 3:43:07 AM

Popular Answer

If I understand the latest documentation for ASP.NET Core 3.1 correctly, we're now expected to use WebApplicationFactory<TStartup>.

You can override its ConfigureWebHost method to change configuration, dependencies, and so on. I recently did this by replacing a real database implementation with a Fake:

public class RestaurantApiFactory : WebApplicationFactory<Startup>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        if (builder is null)
            throw new ArgumentNullException(nameof(builder));

        builder.ConfigureServices(services =>
        {
            var descriptors = services
                .Where(d =>
                    d.ServiceType == typeof(IReservationsRepository))
                .ToList();
            foreach (var d in descriptors)
                services.Remove(d);

            services.AddSingleton<IReservationsRepository>(
                new FakeDatabase());
        });
    }
}

Instead of replacing the your SQL Server-based database implementation with a Fake, you could probably change its connection string as well.

You use the factory like this:

using var factory = new RestaurantApiFactory();
var client = factory.CreateClient();

It looks like you're using xUnit.net. If so, you can use its BeforeAfterTestAttribute to create and delete databases for each test.

Here's the way I usually do it:

public class UseDatabaseAttribute : BeforeAfterTestAttribute
{
    public override void Before(MethodInfo methodUnderTest)
    {
        using (var schemaStream = ReadSchema())
        using (var rdr = new StreamReader(schemaStream))
        {
            var schemaSql = rdr.ReadToEnd();

            var builder = new SqlConnectionStringBuilder(
                ConnectionStrings.Reservations);
            builder.InitialCatalog = "Master";
            using (var conn = new SqlConnection(builder.ConnectionString))
            using (var cmd = new SqlCommand())
            {
                conn.Open();
                cmd.Connection = conn;

                foreach (var sql in SeperateStatements(schemaSql))
                {
                    cmd.CommandText = sql;
                    cmd.ExecuteNonQuery();
                }
            }
        }

        base.Before(methodUnderTest);
    }

    private Stream ReadSchema()
    {
        return typeof(SqlReservationsProgramVisitor<>)
            .Assembly
            .GetManifestResourceStream(
                "Ploeh.Samples.BookingApi.Sql.BookingDbSchema.sql");
    }

    private static IEnumerable<string> SeperateStatements(string schemaSql)
    {
        return schemaSql.Split(
            new[] { "GO" },
            StringSplitOptions.RemoveEmptyEntries);
    }

    public override void After(MethodInfo methodUnderTest)
    {
        base.After(methodUnderTest);

        var dropCmd = @"
            IF EXISTS (SELECT name
                FROM master.dbo.sysdatabases
                WHERE name = N'Booking')
            DROP DATABASE[Booking];";

        var builder = new SqlConnectionStringBuilder(
            ConnectionStrings.Reservations);
        builder.InitialCatalog = "Master";
        using (var conn = new SqlConnection(builder.ConnectionString))
        using (var cmd = new SqlCommand(dropCmd, conn))
        {
            conn.Open();
            cmd.ExecuteNonQuery();
        }
    }
}

You can see it in use here.

0
4/9/2020 8:28:32 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow