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

2024-03-27

关于如何编写良好的单元测试的信息似乎很少actualASP.NET Core 控制器操作。关于如何使这项工作真正发挥作用有什么指导吗?


我有一个系统,现在似乎运行得很好,所以我想我应该分享它,看看它是否能帮助其他人。有一个实体框架文档中非常有用的文章 https://learn.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite这指明了道路。但这是我如何将其合并到实际工作应用程序中的方法。

1. 在您的解决方案中创建 ASP.NET Core Web 应用程序

有大量精彩文章可以帮助您入门。这文档 https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/对于基本设置和脚手架非常有帮助。为此,您需要创建一个具有个人用户帐户的 Web 应用程序,以便将您的 ApplicationDbContext 设置为自动与 EntityFramework 配合使用。

1a.支架控制器

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

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

在您的解决方案中,创建一个新的 .NET Core 库并引用您新创建的 Web 应用程序。在我的示例中,我使用的模型称为Company,并且它使用CompaniesController.

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

对于这个项目,我使用xUnit https://xunit.github.io/作为我的测试跑者,Moq https://github.com/moq/moq用于模拟对象,以及流利断言 http://www.fluentassertions.com/做出更有意义的断言。使用 NuGet 包管理器和/或控制台将这三个库添加到您的项目中。您可能需要使用以下命令来搜索它们Show Prerelease已选中复选框。

您还需要几个包来使用 EntityFramework 的新 Sqlite-InMemory 数据库选项。这是秘密酱汁。以下是 NuGet 上的包名称列表:

  • 微软数据Sqlite
  • 微软实体框架Core.InMemory [强调]
  • 微软实体框架Core.Sqlite [强调]

3. 设置您的测试夹具

根据我之前提到的文章 https://learn.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite,有一种简单、美观的方法可以将 Sqlite 设置为内存中的关系数据库,您可以对其运行测试。

您需要编写单元测试方法,以便每个方法都有一个新的、干净的数据库副本。上面的文章向您展示了如何一次性执行此操作。这是我如何将我的装置设置为DRY https://en.wikipedia.org/wiki/Don't_repeat_yourself尽可能。

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();
        }
    }
    ...
}

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

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

My 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();
        }
    }
}

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

    [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.与数据的异步操作

当然,有时您必须在测试阶段之间来回传递数据。这是我写的一个方法,可以让你做到这一点:

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();
        }
    }
}

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

    [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)
        );
    }

结论

我真的希望这可以帮助人们掌握 C# 和 ASP.NET Core 中令人敬畏的新东西。如果您有任何疑问、批评或建议,请告诉我!我对此也还是个新手,所以任何建设性的反馈对我来说都是无价的!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • window.onload 与

    两者到底有什么区别window onload事件和onload事件的body标签 我什么时候使用哪个以及如何正确完成 window onload myOnloadFunc and 是不同的使用方式同一个事件 Using window onl
  • 如何使用 ScriptTags 为 shopify 开发 Rails 应用程序

    我在 Heroku 中部署了一个 Shopify 应用程序 并使用 Rails 开发 我需要从任何 Shopify 商店的前端调用 JavaScript 函数 我读过这篇文章 http www shopify com technology
  • Appcelerator Titanium:Facebook 图片上传失败

    我的 Titanium 软件中从 Facebook 上传图像时出现错误 每次我想从我的应用程序上传图像时 我都会收到以下信息 失败 v2 1 及更高版本已弃用 REST API 但如果我在 KitchenSink 示例应用程序中尝试相同的代
  • 在设计模式中编辑集合的最简单方法?

    最简单的编辑方法是什么persist像这样的集合decimal or List
  • 在 Python 上分析字符串输入直到达到某个字母

    我需要帮助来尝试编写程序的某个部分 这个想法是 一个人输入一堆乱码 程序会读取它 直到它到达 感叹号 例如 input Type something 人物类型 wolfdo65gtornado salmontiger223 如果我要求程序打
  • `enable_query_strings` 无法正常工作

    我正在尝试使用 CodeIgniter 和 xdebug 当我输入以下 URL 时 http localhost redux index php xdebug 运行良好 当我访问以下网址时 http localhost redux inde
  • QT后台进程进行键盘输入嗅探

    我正在开发一个简单的应用程序 该应用程序将在后台运行并捕获用户的键盘输入 如键盘记录器 但用于 LAN 我正在发送 UDP 数据包来传输击键 但从后台进程捕获键盘输入的问题似乎仍然无法解决 所以需要帮助 如果您想在 Windows 上执行此
  • 子类化 NSNumber

    我想向 NSNumber 类添加一个属性 因此我必须对其进行子类化 文档指出我必须重写所有 NSValue 原始方法 由于 NSValue 文档没有说明哪些方法是原始方法 所以我认为这两个可能是实例化的原始方法 initWithBytes
  • 将文本表转换为 pandas 数据框

    很多时候 当我尝试回答 Stackoverflow 上的问题时 问题包含一个表 我必须将其转换为 pandas 数据框才能进行处理 例如 在这个问题中 http stackoverflow com questions 43172116 pa
  • 使用验证器 jQuery 验证 Click 事件上的表单

    我正在使用 jQuery validate 来验证输入 我的代码 button click function form validate rules phone required true number true rangelength 7
  • 测试 Google Analytics iOS SDK

    有人找到了在 iOS 上测试 Google Analytics 的好方法吗 SDK 非常简单 但文档没有讨论如何测试或验证 该库在模拟器上或运行调试构建配置时的行为是否有所不同 我使用此委托方法设置了委托 GANTrackerDelegat
  • 紧凑框架中的网络浏览器

    我想用一个WebBrowser NET Compact Framework 3 5 项目中的组件 我面临着关于此的相互矛盾的信息 如果我只是尝试使用它 我会得到以下异常 System Threading ThreadStateExcepti
  • 椭圆弧箭头边缘d3力布局

    我正在使用强制布局来创建有向图 它渲染在画布上 我的示例位于http jsbin com vuyapibaqa 1 edit html 输出 http jsbin com vuyapibaqa 1 edit html output 现在我的
  • 无法在 DayNight 主题中动态切换模式

    我在我的应用程序中实现了 DayNight 主题 并添加了一个在白天和夜间模式之间切换的设置 但如果不重新启动 我无法在模式之间动态切换 如果我使用setDefaultNightMode 设置更改后 设置活动不会更改模式 但后台堆栈中的活动
  • VS Code 中的 SQL 调试

    我已经在 Windows 上安装了 VS code 工具并探索其功能 到目前为止 我想说它是一个很棒的工具 与传统的 SSMS 相比 它具有一些很酷的功能 我面临的困难是 在 VS Code 中使用 debug 当我单击调试时 它会在活动栏
  • C:执行 {...} while(0)? [复制]

    这个问题在这里已经有答案了 可能的重复 为什么 C C 宏中有时会出现无意义的 do while 和 if else 语句 https stackoverflow com questions 154136 why are there som
  • 为什么 SQL Server 2008 Management Studio Intellisense 不工作?

    我正在疯狂地试图找出为什么智能感知根本无法工作 我使用的服务器是本地的 并且是 2008 年的 数据库设置为 2008 兼容性 智能感知在我能找到的每个菜单中都打开 但即使使用 CTRL J 也不会弹出任何成员列表 有没有人经历过类似的事情
  • Chrome Timeline 开发工具中的图像解码时间

    我正在构建一个视差滚动网站 不是我们所有人吗 除其他外 它会在用户滚动时显示图像 我通过将图像放在背景中并在顶部放置一个实心填充的 div 来完成 显示 然后 我根据滚动位置将该 div 从 100 高度设置为 0 高度 从而显示背景图像
  • 是否可以在 ng 服务之前和 Angular 7 中的实时重新加载/自动重新加载之前运行自定义脚本?

    在从 Angular cli 启动 ngserve 命令之前以及在 Angular 实时重新加载 自动重新加载之前 我必须运行自定义脚本吗 除了修改 package json 之外 是否可以使用 Angular 7 找你的package j
  • 如何为实际使用数据库上下文的 ASP.NET Core 控制器编写单元测试?

    关于如何编写良好的单元测试的信息似乎很少actualASP NET Core 控制器操作 关于如何使这项工作真正发挥作用有什么指导吗 我有一个系统 现在似乎运行得很好 所以我想我应该分享它 看看它是否能帮助其他人 有一个实体框架文档中非常有