如何为实际使用我的数据库上下文的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中令人敬畏的新东西。 如果您有任何问题,批评或建议,请告诉我! 我也是新手,所以任何建设性的反馈对我来说都是无价之宝!
- .Net Core 2.0 Windows服务
- 托管ASP.NET Core作为Windows服务
- 升级到v1.1后,ASP.NET Core将无法在IIS中运行
- ASP.NET Core Web API:程序不包含适用于入口点的静态“Main”方法
- .NET Core – 尝试将存储库添加到我的API控制器,但是当我执行每个控制器方法时都会返回500错误
- AspNetCore.Identity无法使用自定义用户/角色实现
- .net核心中的GZIP无法正常工作
- 如何在asp.net核心中编写自定义actionResult
- ASP.NET MVC 6中MVC Controller和Web API Controller有什么区别?