ASP.NET Core 集成测试中结合 WebApplicationFactory 使用 SQLite 内存数据库
SQLite 内存数据库(in-memory database)的连接字符串是 Data Source=:memory: ,它的特点是数据库连接一关闭,数据库就会被删除。而使用 services.AddDbContext 通过连接字符串配置 EF Core 时,EF Core 会在每次查询或 SaveChanges 后立即关闭数据库连接。在这样的情况下,集成测试中就无法在向 SQLite 内存数据库写入数据库后进行查询测试。
为了解决上述问题,我们就不能让 EF Core 自己自动维护数据库连接,而只能改为手动模式,手工创建并打开 SqliteConnection 给 EF Core 使用,在用完之后的适当时候关闭连接。
除此之外,由于在每次打开数据库连接都会创建新的数据库,所以还要解决在什么写入数据之前完成数据库的初始化。
结合 WebApplicationFactory ,我们用下面继承自 WebApplicationFactory 的实现代码解决了问题。
public class BlogWebAppFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class { private DbConnection _dbConnection; public BlogWebAppFactory() { } protected override void ConfigureWebHost(IWebHostBuilder builder) { base.ConfigureWebHost(builder); builder.ConfigureServices(services => { _dbConnection = new SqliteConnection("Data Source=:memory:"); _dbConnection.Open(); services.AddDbContext<EfUnitOfWork>(options => { options.UseSqlite(_dbConnection); }); }); } protected override TestServer CreateServer(IWebHostBuilder builder) { var server = base.CreateServer(builder); using (var scope = server.Host.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService<EfUnitOfWork>(); dbContext.Database.EnsureCreated(); } return server; } protected override void Dispose(bool disposing) { base.Dispose(disposing); _dbConnection?.Dispose(); } }
集成测试中的示例代码如下
public class PostsWebApiTests : IClassFixture<BlogWebAppFactory<Startup>> { private readonly BlogWebAppFactory<Startup> _factory; private readonly HttpClient _httpClient; public PostsWebApiTests(BlogWebAppFactory<Startup> factory) { _factory = factory; _httpClient = factory.CreateClient(); } [Fact] public async Task GetPostsByBlogIdsTest() { var fakePosts = SeedData(); var blogIds = fakePosts.Select(p => p.BlogID).Distinct(); var response = await _httpClient.PostAsJsonAsync($"/blogposts/blogIds", blogIds); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } private IList<BlogPost> SeedData() { using (var scope = _factory.Server.Host.Services.CreateScope()) { var efUnitOfWork = scope.ServiceProvider.GetRequiredService<EfUnitOfWork>(); var blogSites = Builder<BlogSite>.CreateListOfSize(3).All() .Do(b => b.BlogID = 0) .Build(); var fakePosts = Builder<BlogPost>.CreateListOfSize(12).All() .Do(x => x.Id = 0) .TheFirst(4).With(x => x.BlogSite = blogSites[0]) .TheNext(4).With(x => x.BlogSite = blogSites[1]) .TheNext(4).With(x => x.BlogSite = blogSites[2]) .Build(); efUnitOfWork.AddRange(fakePosts); efUnitOfWork.SaveChanges(); return fakePosts; } } }