我有一个系统,现在似乎运行得很好,所以我想我应该分享它,看看它是否能帮助其他人。有一个实体框架文档中非常有用的文章 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 中令人敬畏的新东西。如果您有任何疑问、批评或建议,请告诉我!我对此也还是个新手,所以任何建设性的反馈对我来说都是无价的!