在Azure DevOps中,使用.net Core和EF Core,我采用了一种不同的技术。我使用SQLite内存数据库来执行集成测试和端到端测试。目前在.net Core中,您可以使用InMemory数据库和带有内存选项的SQLite,在默认的Azure DevOps CI代理中运行任何集成测试。
InMemory:
https://learn.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory
请注意,InMemory数据库不是关系数据库,它是一个多用途数据库,只提及其中一种限制:
"InMemory将允许您保存违反关系数据库引用完整性约束的数据"
SQLite内存模式
https://learn.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite
这种方法为测试提供了更真实的平台。
现在,我进一步进行了操作,不仅希望能够在Azure DevOps中运行具有数据库依赖性的集成测试,还希望能够在CI Agent中托管我的WebAPI,并在API DBcontext和我的Persister对象之间共享数据库(Persister对象是一个帮助类,允许我自动生成任何类型的实体并将它们保存到数据库)。
关于集成测试和端到端测试的简要说明:
集成测试
涉及数据库的集成测试示例可以是数据访问层的测试。在这种情况下,通常情况下,在开始测试时会创建一个DBContext,用一些数据填充目标数据库,使用待测组件来操作数据,再次使用DBContext确保断言得到满足。
这种情况非常直接,在同一代码中,您可以共享相同的DBContext来生成数据并将其注入到组件中。
端到端测试
假设您有像我一样的RESTful .net Core WebAPI需要测试,确保所有CRUD操作都按预期工作,并且您还想测试过滤、分页等是否正确。
在这种情况下,更复杂的是在测试(数据设置和/或验证)和WebAPI堆栈之间共享相同的DBContext。
在 .net EF Core 和 WebHostBuilder 之前
到目前为止,我所知道的唯一方法是拥有一个专用服务器、虚拟机或 docker 镜像,负责提供 API,并且也必须可以从 Web 或 Azure DevOps 访问。设置我的集成测试,以重新创建数据库,或足够聪明/有限制地忽略现有数据,并确保每个测试对数据损坏具有弹性并且完全可靠(没有错误的负面或正面结果)。然后我必须配置我的构建定义来运行测试。
利用 SQLite 内存缓存=共享和 WebHostBuilder
下面首先描述我使用的两种主要技术,然后添加一些代码来展示如何实现它。
SQLite file::memory:?cache=shared
SQLite允许您在内存中工作,而不是使用传统文件,这已经为我们提供了巨大的性能提升,消除了I/O瓶颈。但除此之外,使用选项cache=shared,我们可以在同一进程内使用多个连接访问相同的数据。如果您需要多个数据库,可以指定名称。
更多信息:https://www.sqlite.org/inmemorydb.html
WebHostBuilder
.NET Core提供了主机构建器, WebHostBuilder 允许我们创建一个服务器,启动并托管我们的WebAPI,以便像在真实服务器上托管一样访问它们。
当您在测试类中使用WebHostBuilder时,这两个都存在于同一进程中。
更多信息:https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder?view=aspnetcore-2.2
解决方案
当初始化端到端测试时,创建一个新客户端连接api,创建一个dbcontext,您将使用它来填充数据库并进行断言。
测试初始化:
[TestClass]
public class CategoryControllerTests
{
private TestServerApiClient _client;
private Persister<Category> _categoryPersister;
private Builder<Category> _categoryBuilder;
private IHouseKeeperContext _context;
protected IDbContextTransaction Transaction;
[TestInitialize]
public void TestInitialize()
{
_context = ContextProvider.GetContext();
_client = new TestServerApiClient();
ContextProvider.ResetDatabase();
_categoryPersister = new Persister<Category>(_context);
_categoryBuilder = new Builder<Category>();
}
[TestCleanup]
public void Cleanup()
{
_client?.Dispose();
_context?.Dispose();
_categoryPersister?.Dispose();
ContextProvider.Dispose();
}
[...]
}
TestServerApiClient 类:
public class TestServerApiClient : System.IDisposable
{
private readonly HttpClient _client;
private readonly TestServer _server;
public TestServerApiClient()
{
var webHostBuilder = new WebHostBuilder();
webHostBuilder.UseEnvironment("Test");
webHostBuilder.UseStartup<Startup>();
_server = new TestServer(webHostBuilder);
_client = _server.CreateClient();
}
public void Dispose()
{
_server?.Dispose();
_client?.Dispose();
}
}
ContextProvider类用于生成DBContext,可以用于种子数据或执行数据库查询以进行断言。
public static class ContextProvider
{
private static bool _requiresDbDeletion;
private static IConfiguration _applicationConfiguration;
public static IConfiguration ApplicationConfiguration
{
get
{
if (_applicationConfiguration != null) return _applicationConfiguration;
_applicationConfiguration = new ConfigurationBuilder()
.AddJsonFile("Config/appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
return _applicationConfiguration;
}
}
private static ServiceProvider _serviceProvider;
public static ServiceProvider ServiceProvider
{
get
{
if (_serviceProvider != null) return _serviceProvider;
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IConfiguration>(ApplicationConfiguration);
var databaseType = ApplicationConfiguration?.GetValue<DatabaseType>("DatabaseType") ?? DatabaseType.SQLServer;
_requiresDbDeletion = databaseType == DatabaseType.SQLServer;
IocConfig.RegisterContext(serviceCollection, null);
_serviceProvider = serviceCollection.BuildServiceProvider();
return _serviceProvider;
}
set
{
_serviceProvider = value;
}
}
public static IHouseKeeperContext GetContext()
{
return ServiceProvider.GetService<IHouseKeeperContext>();
}
public static void Dispose()
{
ServiceProvider?.Dispose();
ServiceProvider = null;
}
public static void ResetDatabase()
{
if (_requiresDbDeletion)
{
GetContext()?.Database?.EnsureDeleted();
GetContext()?.Database?.EnsureCreated();
}
}
}
IocConfig类是我在我的框架中使用的帮助类,用于设置依赖注入。上面使用的方法RegisterContext负责注册DBContext并根据需要进行设置,因为这是WebAPI使用的同一类,所以使用配置的DatabaseType确定要做什么。
在这个类里面,你可能会发现大部分的“复杂性”。
当在内存中使用SQLite时,你需要记住以下几点:
- 连接不像使用SQL Server那样自动打开和关闭(这就是为什么我使用了:
context.Database.OpenConnection();
)
- 如果没有活动连接,则数据库将被删除(这就是为什么我使用了
services.AddSingleton<IHouseKeeperContext>(s ...
重要的是留下一个连接保持打开状态,以便数据库不会被销毁,但另一方面,你必须小心关闭所有连接,以便数据库最终被销毁,并且下一个测试将正确地创建一个新的空数据库。
班级的其余部分处理了生产和测试设置的SQL Server配置。我可以随时设置测试使用真实的SQL Server实例,所有测试将保持完全独立,但速度肯定会很慢,可能只适用于夜间构建(如果需要,并且取决于您的系统大小)。
public class IocConfig
{
public static void RegisterContext(IServiceCollection services, IHostingEnvironment hostingEnvironment)
{
var serviceProvider = services.BuildServiceProvider();
var configuration = serviceProvider.GetService<IConfiguration>();
var connectionString = configuration.GetConnectionString(Constants.ConfigConnectionStringName);
var databaseType = DatabaseType.SQLServer;
try
{
databaseType = configuration?.GetValue<DatabaseType>("DatabaseType") ?? DatabaseType.SQLServer;
}catch
{
MyLoggerFactory.CreateLogger<IocConfig>()?.LogWarning("Missing or invalid configuration: DatabaseType");
databaseType = DatabaseType.SQLServer;
}
if(hostingEnvironment != null && hostingEnvironment.IsProduction())
{
if(databaseType == DatabaseType.SQLiteInMemory)
{
throw new ConfigurationErrorsException($"Cannot use database type {databaseType} for production environment");
}
}
switch (databaseType)
{
case DatabaseType.SQLiteInMemory:
services.AddDbContext<HouseKeeperContext>(options =>
{
options.UseSqlite($"DataSource='file::memory:?cache=shared'");
});
services.AddSingleton<IHouseKeeperContext>(s => {
var context = s.GetService<HouseKeeperContext>();
context.Database.OpenConnection();
context.Database.EnsureCreated();
return context;
});
break;
case DatabaseType.SQLServer:
default:
if (hostingEnvironment == null || hostingEnvironment.IsTesting())
{
services.AddDbContext<HouseKeeperContext>(options =>
{
options.UseSqlServer(connectionString);
});
services.AddSingleton<IHouseKeeperContext>(s => {
var context = s.GetService<HouseKeeperContext>();
context.Database.EnsureCreated();
return context;
});
break;
}
services.AddDbContextPool<HouseKeeperContext>(options =>
{
options.UseSqlServer(connectionString);
options.UseLoggerFactory(MyLoggerFactory);
}, poolSize: 5);
services.AddTransient<IHouseKeeperContext>(service =>
services.BuildServiceProvider()
.GetService<HouseKeeperContext>());
break;
}
}
[...]
}
示例测试,首先我使用持久器生成数据并将其种植在数据库中,然后我使用API获取数据,测试也可以反转,使用POST请求设置数据,然后使用DBContext读取数据库并确保创建成功。
[TestMethod]
public async Task GET_support_orderBy_Id()
";
c.Description = $"Desc_i";
});
var response = await _client.GetAsync("/api/category?&orderby=Id");
var categories = response.To<List<Category>>();
Assert.That.All(categories).HaveCount(3);
Assert.IsTrue(categories[0].Id < categories[1].Id &&
categories[1].Id < categories[2].Id);
response = await _client.GetAsync("/api/category?$orderby=Id desc");
categories = response.To<List<Category>>();
Assert.That.All(categories).HaveCount(3);
Assert.IsTrue(categories[0].Id > categories[1].Id &&
categories[1].Id > categories[2].Id);
}
结论
我很喜欢可以在Azure DevOps免费运行E2E测试,性能非常好,这给了我很多信心,特别是当您想要设置持续交付环境时。这是 Azure DevOps(免费版)中此代码部分构建执行的屏幕截图。
![enter image description here](https://istack.dev59.com/mkDto.webp)
很抱歉这篇文章比预期的要长。