如何为实际使用我的数据库上下文的ASP.NET核心控制器编写unit testing?

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

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

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

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

1A。 脚手架一个控制器

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

2.为unit testing创​​建单独的类库

在您的解决方案中,创建一个新的.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设置为内存中的关系数据库,您可以对其进行测试。

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

3A。 同步控制器动作

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

 public class TestFixture { public SqliteConnection ConnectionFactory() => new SqliteConnection("DataSource=:memory:"); public DbContextOptions DbOptionsFactory(SqliteConnection connection) => new DbContextOptionsBuilder() .UseSqlite(connection) .Options; public Company CompanyFactory() => new Company {Name = Guid.NewGuid().ToString()}; public void RunWithDatabase( Action arrange, Func act, Action 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(); } } ... } 

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

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

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

3B。 异步控制器操作

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

 public class TestFixture { ... public async Task RunWithDatabaseAsync( Func arrange, Func> act, Action 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() ); } 

3C。 与数据的异步操作

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

 public class TestFixture { ... public async Task RunWithDatabaseAsync( Func> arrange, Func> act, Action 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() .ViewData.ModelState.Keys.Should().Contain((string) data.Key) ); } 

结论

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