如何为实际使用我的数据库上下文的ASP.NET Core控制器编写单元测试?

the*_*utz 5 c# asp.net-mvc unit-testing entity-framework-core asp.net-core

似乎没有关于如何为实际的 ASP.NET核心控制器操作编写好的单元测试的信息.有关如何使这项工作真实的任何指导?

the*_*utz 9

我有一个似乎现在工作得很好的系统,所以我想我会分享它,看看它是否对其他人没有帮助.实体框架文档中有一篇非常有用的文章指明了方向.但这是我如何将其纳入实际的工作应用程序.

1.在解决方案中创建ASP.NET Core Web App

有很多很棒的文章可以帮助你入门.基本设置和脚手架的文档非常有用.为此,您需要创建一个包含个人用户帐户的Web应用程序,以便您的ApplicationDbContext设置为自动使用EntityFramework.

1A.脚手架控制器

使用文档中包含的信息创建一个具有基本CRUD操作的简单控制器.

2.为单元测试创​​建单独的类库

在您的解决方案中,创建一个新的.NET Core Library并引用您新创建的Web应用程序.在我的例子中,我正在使用的模型被调用Company,它使用了CompaniesController.

2A.将必要的包添加到测试库中

对于这个项目,我使用xUnit作为我的测试运行器,使用Moq模拟对象,使用FluentAssertions来创建更有意义的断言.使用NuGet Package Manager和/或Console将这三个库添加到项目中.您可能需要Show Prerelease选中复选框来搜索它们.

您还需要一些包来使用EntityFramework的新Sqlite-InMemory数据库选项.这是秘诀.以下是NuGet上的软件包名称列表:

  • Microsoft.Data.Sqlite
  • Microsoft.EntityFramework Core .InMemory [强调添加]
  • Microsoft.EntityFramework Core .Sqlite [强调添加]

3.设置您的测试夹具

根据我之前提到的文章,有一种简单,漂亮的方法可以将Sqlite设置为内存中的关系数据库,您可以对其进行测试.

您将要编写单元测试方法,以便每个方法都有一个新的,干净的数据库副本.上面的文章向您展示了如何一次性完成这项工作.以下是我将夹具设置为尽可能干燥的方法.

3A.同步控制器动作

我编写了以下方法,允许我使用Arrange/Act/Assert模型编写测试,每个阶段在我的测试中充当参数.下面是方法的代码及其TestFixture引用的相关类属性,最后是调用代码的示例.

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();
        }
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

这是调用代码来测试Create方法的样子CompaniesController(我使用参数名称来帮助我保持表达式的直接,但你并不严格需要它们):

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

我的CompaniesController类需要一个记录器,我用Moq模拟并在我的TestFixture中存储为变量.

3B.异步控制器操作

当然,许多内置的ASP.NET Core操作都是异步的.为了使用这个结构,我写了下面的方法:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它几乎完全相同,只是使用异步方法和等待设置.下面是调用这些方法的示例:

    [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>()
        );
    }
Run Code Online (Sandbox Code Playgroud)

3C.与数据的异步操作

当然,有时您必须在测试阶段之间来回传递数据.这是我写的一个方法,允许你这样做:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,我是如何使用此代码的示例:

    [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)
        );
    }
Run Code Online (Sandbox Code Playgroud)

结论

我真的希望这能帮助一些人在C#和ASP.NET Core中出色的新东西站起来.如果您有任何问题,批评或建议,请告诉我!我也是新手,所以任何建设性的反馈对我来说都是无价之宝!

  • 这是一项有趣且有用的工作,但我认为重要的是要认识到您所创建的是集成测试,而不是单元测试.这本身并不是一件坏事,但有一些很好的理由让单元测试和集成测试分开.此外,只要您严格遵守IOC和关注点概念,单元测试控制器在大多数情况下都非常简单和充分. (7认同)