【发布时间】:2020-04-01 05:26:38
【问题描述】:
我正在学习 ASP.NET Core 3 并构建了一个基本应用程序。我正在寻找运行集成测试以正确断言对从数据库读取/写入的控制器的调用。为了避免依赖实际数据库,我正在考虑使用 EF Core 的内存数据库。我一直关注this 文章作为我的主要指南。
我遇到的问题是我正在努力确保每个单独的集成测试都使用新的数据库上下文。
最初,我多次调用我的数据库种子方法时遇到错误(第二次和后续调用未能添加重复的主键 - 本质上它使用的是相同的上下文)。
通过查看这里的各种博客、教程和其他问题,我通过使用唯一名称(使用 Guid.NewGuid())实例化内存数据库来解决这个问题。这应该解决了我的问题。然而,这给了我一个不同的问题。每次测试初始化时都正确调用了数据库种子方法,但是当我随后调用控制器操作时,依赖注入实例化了一个 new 数据库上下文,这意味着不存在任何种子数据!
我似乎在绕圈子,要么只能调用一次种子数据,并且只能进行一次测试,要么进行多个测试但没有种子数据!
我已经对 DbContext 服务的作用域生命周期进行了试验,将其设置为瞬态/作用域/单例,但结果似乎没有差异。
我设法让它工作的唯一方法是在种子方法中添加对db.Database.EnsureDeleted() 的调用之前调用db.Database.EnsureCreated(),但这似乎是一个巨大的黑客和感觉不对。
下面发布的是我的实用程序类,用于为测试设置内存数据库和一个测试类。希望这已经足够了,因为我觉得这篇文章已经足够长了,但如果需要,可以发布实际的控制器/启动类(尽管它们相当普通)。
非常感谢任何帮助。
用于设置内存数据库的实用程序类
using CompetitionStats.Entities;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
namespace CompetitionStatsUnitTests
{
class Utilities
{
internal class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Remove the app's ApplicationDbContext registration.
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<CompetitionStatsContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
// Add ApplicationDbContext using an in-memory database for testing.
services.AddDbContext<CompetitionStatsContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
});
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<CompetitionStatsContext>();
var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
db.Database.EnsureDeleted(); // feels hacky - don't think this is good practice, but does achieve my intention
db.Database.EnsureCreated();
try
{
InitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding the database with test messages. Error: {Message}}", ex.Message);
}
}
});
}
private static void InitializeDbForTests(CompetitionStatsContext db)
{
db.Teams.Add(new CompetitionStats.Models.TeamDTO
{
Id = new Guid("3b477978-f280-11e9-8490-a8667f2f93c4"),
Name = "Arsenal"
});
db.SaveChanges();
}
}
}
}
测试类
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Net.Http;
using System.Threading.Tasks;
namespace CompetitionStatsUnitTests.ControllerUnitTests
{
[TestClass]
public class TeamControllerTest
{
private HttpClient _testClient;
[TestInitialize]
public void Initialize()
{
var factory = new Utilities.CustomWebApplicationFactory<CompetitionStats.Startup>();
this._testClient = factory.CreateClient();
}
[TestMethod]
public async Task TeamController_GetTeam_Returns_Team()
{
var actualResponse = await this._testClient.GetStringAsync("api/teams/3b477978-f280-11e9-8490-a8667f2f93c4");
var expectedResponse = @"{""id"":""3b477978-f280-11e9-8490-a8667f2f93c4"",""name"":""Arsenal""}";
Assert.AreEqual(expectedResponse, actualResponse);
}
[TestMethod]
public async Task TeamController_PostTeam_Adds_Team()
{
var content = new StringContent(@"{""Name"": ""Liverpool FC""}", System.Text.Encoding.UTF8, "application/json");
var response = await this._testClient.PostAsync("api/teams/", content);
Assert.AreEqual(response.StatusCode, System.Net.HttpStatusCode.Created);
}
}
}
【问题讨论】:
标签: asp.net entity-framework asp.net-core-mvc entity-framework-core integration-testing