Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I write unit tests for ASP.NET Core controllers that actually use my Database Context?

There seems to be little information about how to write good unit tests for actual ASP.NET Core controller actions. Any guidance about how to make this work for real?

like image 785
theutz Avatar asked Dec 03 '16 16:12

theutz


1 Answers

I've got a system that seems to be working pretty well right now, so I thought I'd share it and see if it doesn't help someone else out. There's a really useful article in the Entity Framework documentation that points the way. But here's how I incorporated it into an actual working application.

1. Create an ASP.NET Core Web App in your solution

There are tons of great articles out there to help you get started. The documentation for basic setup and scaffolding is very helpful. For this purpose, you'll want to create a web app with Individual User Accounts so that your ApplicationDbContext is setup to work with EntityFramework automatically.

1a. Scaffold a controller

Use the information included in the documentation to create a simple controller with basic CRUD actions.

2. Create a separate class library for your unit tests

In your solution, create a new .NET Core Library and reference your newly created web app. In my example, the model I'm using is called Company, and it uses the CompaniesController.

2a. Add the necessary packages to your test library

For this project, I use xUnit as my test runner, Moq for mocking objects, and FluentAssertions to make more meaningful assertions. Add those three libraries to your project using NuGet Package Manager and/or Console. You may need to search for them with the Show Prerelease checkbox selected.

You will also need a couple of packages to use EntityFramework's new Sqlite-InMemory database option. This is the secret sauce. Below are a list of the package names on NuGet:

  • Microsoft.Data.Sqlite
  • Microsoft.EntityFrameworkCore.InMemory [emphasis added]
  • Microsoft.EntityFrameworkCore.Sqlite [emphasis added]

3. Setup Your Test Fixture

Per the article I mentioned earlier, there is a simple, beautiful way to set up Sqlite to work as an in-memory, relational database which you can run your tests against.

You'll want to write your unit test methods so that each method has a new, clean copy of the database. The article above shows you how to do that on a one-off basis. Here's how I set up my fixture to be as DRY as possible.

3a. Synchronous Controller Actions

I've written the following method that allows me to write tests using the Arrange/Act/Assert model, with each stage acting as a parameter in my test. Below is the code for the method and the relevant class properties in the TestFixture that it references, and finally an example of what it looks like to call the code.

public class TestFixture {
    public SqliteConnection ConnectionFactory() => new SqliteConnection("DataSource=:memory:");

    public DbContextOptions<ApplicationDbContext> DbOptionsFactory(SqliteConnection connection) =>
        new DbContextOptionsBuilder<ApplicationDbContext>()
        .UseSqlite(connection)
        .Options;

    public Company CompanyFactory() => new Company {Name = Guid.NewGuid().ToString()};

    public void RunWithDatabase(
        Action<ApplicationDbContext> arrange,
        Func<ApplicationDbContext, IActionResult> act,
        Action<IActionResult> assert)
    {
        var connection = ConnectionFactory();
        connection.Open();

        try
        {
            var options = DbOptionsFactory(connection);

            using (var context = new ApplicationDbContext(options))
            {
                context.Database.EnsureCreated();
                // Arrange
                arrange?.Invoke(context);
            }

            using (var context = new ApplicationDbContext(options))
            {
                // Act (and pass result into assert)
                var result = act.Invoke(context);
                // Assert
                assert.Invoke(result);
            }
        }
        finally
        {
            connection.Close();
        }
    }
    ...
}

Here's what it looks like to call the code to test the Create method on the CompaniesController (I use parameter names to help me keep my expressions straight, but you don't strictly need them):

    [Fact]
    public void Get_ReturnsAViewResult()
    {
        _fixture.RunWithDatabase(
            arrange: null,
            act: context => new CompaniesController(context, _logger).Create(), 
            assert: result => result.Should().BeOfType<ViewResult>()
        );
    }

My CompaniesController class requires a logger, that I mock up with Moq and store as a variable in my TestFixture.

3b. Asynchronous Controller Actions

Of course, many of the built-in ASP.NET Core actions are asynchronous. To use this structure with those, I've written the method below:

public class TestFixture {
    ...
    public async Task RunWithDatabaseAsync(
        Func<ApplicationDbContext, Task> arrange,
        Func<ApplicationDbContext, Task<IActionResult>> act,
        Action<IActionResult> assert)
    {
        var connection = ConnectionFactory();
        await connection.OpenAsync();

        try
        {
            var options = DbOptionsFactory(connection);

            using (var context = new ApplicationDbContext(options))
            {
                await context.Database.EnsureCreatedAsync();
                if (arrange != null) await arrange.Invoke(context);
            }

            using (var context = new ApplicationDbContext(options))
            {
                var result = await act.Invoke(context);
                assert.Invoke(result);
            }
        }
        finally
        {
            connection.Close();
        }
    }
}

It's almost exactly the same, just setup with async methods and awaiters. Below, an example of calling these methods:

    [Fact]
    public async Task Post_WhenViewModelDoesNotMatchId_ReturnsNotFound()
    {
        await _fixture.RunWithDatabaseAsync(
            arrange: async context =>
            {
                context.Company.Add(CompanyFactory());
                await context.SaveChangesAsync();
            },
            act: async context => await new CompaniesController(context, _logger).Edit(1, CompanyFactory()),
            assert: result => result.Should().BeOfType<NotFoundResult>()
        );
    }

3c. Async Actions with Data

Of course, sometimes you'll have to pass data back-and-forth between the stages of testing. Here's a method I wrote that allows you to do that:

public class TestFixture {
    ...
    public async Task RunWithDatabaseAsync(
        Func<ApplicationDbContext, Task<dynamic>> arrange,
        Func<ApplicationDbContext, dynamic, Task<IActionResult>> act,
        Action<IActionResult, dynamic> assert)
    {
        var connection = ConnectionFactory();
        await connection.OpenAsync();

        try
        {
            object data;
            var options = DbOptionsFactory(connection);

            using (var context = new ApplicationDbContext(options))
            {
                await context.Database.EnsureCreatedAsync();
                data = arrange != null 
                    ? await arrange?.Invoke(context) 
                    : null;
            }

            using (var context = new ApplicationDbContext(options))
            {
                var result = await act.Invoke(context, data);
                assert.Invoke(result, data);
            }
        }
        finally
        {
            connection.Close();
        }
    }
}

And, of course, an example of how I use this code:

    [Fact]
    public async Task Post_WithInvalidModel_ReturnsModelErrors()
    {
        await _fixture.RunWithDatabaseAsync(
            arrange: async context =>
            {
                var data = new
                {
                    Key = "Name",
                    Message = "Name cannot be null",
                    Company = CompanyFactory()
                };
                context.Company.Add(data.Company);
                await context.SaveChangesAsync();
                return data;
            },
            act: async (context, data) =>
            {
                var ctrl = new CompaniesController(context, _logger);
                ctrl.ModelState.AddModelError(data.Key, data.Message);
                return await ctrl.Edit(1, data.Company);
            },
            assert: (result, data) => result.As<ViewResult>()
                .ViewData.ModelState.Keys.Should().Contain((string) data.Key)
        );
    }

Conclusion

I really hope this helps somebody getting on their feet with C# and the awesome new stuff in ASP.NET Core. If you have any questions, criticisms, or suggestions, please let me know! I'm still new at this, too, so any constructive feedback is invaluable to me!

like image 52
theutz Avatar answered Nov 15 '22 17:11

theutz