【问题标题】:Seed test data for every test in ASP.NET Core / EF Core / xUnit.NET integration tests为 ASP.NET Core / EF Core / xUnit.NET 集成测试中的每个测试播种测试数据
【发布时间】:2019-09-12 15:14:18
【问题描述】:

我一直在遵循使用 Microsoft 文档 Integration tests in ASP.NET Core 为 ASP.NET Core 2.2 API 设置测试的策略。

总而言之,我们扩展和自定义WebApplicationFactory 并使用IWebHostBuilder 来设置和配置各种服务,以便为我们提供使用内存数据库进行测试的数据库上下文,如下所示(从文章中复制和粘贴) :

public class CustomWebApplicationFactory<TStartup> 
    : WebApplicationFactory<TStartup> where TStartup: class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Create a new service provider.
            var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkInMemoryDatabase()
                .BuildServiceProvider();

            // Add a database context (ApplicationDbContext) using an in-memory 
            // database for testing.
            services.AddDbContext<ApplicationDbContext>(options => 
            {
                options.UseInMemoryDatabase("InMemoryDbForTesting");
                options.UseInternalServiceProvider(serviceProvider);
            });

            // 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<ApplicationDbContext>();
                var logger = scopedServices
                    .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();

                // Ensure the database is created.
                db.Database.EnsureCreated();

                try
                {
                    // Seed the database with test data.
                    Utilities.InitializeDbForTests(db);
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, $"An error occurred seeding the " +
                        "database with test messages. Error: {ex.Message}");
                }
            }
        });
    }
}

在测试中我们可以使用工厂并像这样创建一个客户端:

public class IndexPageTests : 
    IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> 
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<RazorPagesProject.Startup> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
    }

    [Fact]
    public async Task Test1()
    {
        var response = await _client.GetAsync("/api/someendpoint");
    }
}

这很好用,但请注意对InitializeDbForTests 的调用,它会在配置服务时为所有测试设置一些测试数据。

我想要一个合理的策略,让每个 API 测试都从头开始,这样测试就不会相互依赖。我一直在寻找各种方法来在我的测试方法中获取ApplicationDbContext,但无济于事。

在彼此完全隔离的情况下进行集成测试是否合理,我如何使用 ASP.NET Core / EF Core / xUnit.NET 来处理它?

【问题讨论】:

    标签: c# asp.net asp.net-core integration-testing xunit.net


    【解决方案1】:

    好的,所以我开始工作了!获得范围服务是关键。当我想从头开始播种时,我可以通过将播种调用包装在 using (var scope = _factory.Server.Host.Services.CreateScope()) { } 部分开始每个测试,我可以首先在 var scopedServices = scope.ServiceProvider;var db = scopedServices.GetRequiredService&lt;MyDbContext&gt;(); 之前 db.Database.EnsureDeleted() 并最后运行我的播种功能。有点笨拙,但它有效。

    感谢 Chris Pratt 的帮助(来自评论的回答)。

    【讨论】:

      【解决方案2】:

      实际上,Testing with InMemory 在标题为“编写测试”的部分中很好地描述了该过程。这是一些说明基本思想的代码

          [TestClass]
      public class BlogServiceTests
      {
          [TestMethod]
          public void Add_writes_to_database()
          {
              var options = new DbContextOptionsBuilder<BloggingContext>()
                  .UseInMemoryDatabase(databaseName: "Add_writes_to_database")
                  .Options;
      

      这个想法是每个测试方法都有一个单独的数据库,因此您不必担心测试运行的顺序或它们并行运行的事实。当然,您必须添加一些代码来填充您的数据库并从每个测试方法中调用它。

      我已经使用过这种技术并且效果很好。

      【讨论】:

        【解决方案3】:

        具有讽刺意味的是,您正在寻找 EnsureDeleted 而不是 EnsureCreated。这将转储数据库。由于内存中的“数据库”是无模式的,因此您实际上不需要确保它已创建甚至迁移。

        此外,您不应为内存数据库使用硬编码名称。这实际上会导致内存中的相同数据库实例在任何地方使用。相反,您应该随机使用:Guid.NewGuid().ToString() 就足够了。

        【讨论】:

        • 那么我的问题是我会在哪里调用该方法?据我了解, InitializeDbForTests 只运行一次,我无法直接从测试中访问上下文对象。也许我误解了什么?根据您的第二条评论,如果数据库被赋予一个随机名称,它会从头开始重新初始化吗?
        • 您在当前调用EnsureCreated 的同一个地方调用它:本质上,在您想用数据填充它之前。
        • 好的,谢谢。我想问的是如何为每个测试运行一次 InitializeDbForTests (或等效功能),而不是在运行所有测试之前只运行一次。正如上面链接中的示例所示,一个夹具中的所有方法共享相同的种子数据,因此可能会相互影响。
        • 明白了。是的,只需将其从出厂设置中拉出并放入您的测试设置(测试类构造函数)中。工厂有一个Host 成员,它本身有一个Services 成员——IServiceProvider 的一个实例,即_factory.Host.Services.GetRequiredService&lt;Foo&gt;()
        • 刚刚意识到我在那个例子上过于简单化了。上下文当然是作用域的,所以你实际上需要做_factory.Host.Services.CreateScope()
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-04-01
        • 1970-01-01
        • 2018-04-10
        • 1970-01-01
        • 2020-06-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多