【问题标题】:Resetting In-Memory database between integration tests在集成测试之间重置 In-Memory 数据库
【发布时间】:2020-03-04 09:23:35
【问题描述】:

我已经建立了一个基于https://github.com/jasontaylordev/CleanArchitecture 的项目。但是我在为控制器编写集成测试时遇到了一些麻烦,因为内存数据库不会在每个测试之间重置。每个测试都使用 WebApplicationFactory 来设置测试 Web 服务器,

public 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<ApplicationDbContext>));

                    if (descriptor != null)
                    {
                        services.Remove(descriptor);
                    }

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

                    // Register test services
                    services.AddScoped<ICurrentUserService, TestCurrentUserService>();
                    services.AddScoped<IDateTime, TestDateTimeService>();
                    services.AddScoped<IIdentityService, TestIdentityService>();

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

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

                        try
                        {
                            // Seed the database with test data.
                            SeedSampleData(context);
                        }
                        catch (Exception ex)
                        {
                            logger.LogError(ex, "An error occurred seeding the database with test messages. Error: {Message}", ex.Message);
                        }
                    }
                })
                .UseEnvironment("Test");
        }
    ...

其中创建测试如下:

namespace CleanArchitecture.WebUI.IntegrationTests.Controllers.TodoItems
{
    public class Create : IClassFixture<CustomWebApplicationFactory<Startup>>
    {
        private readonly CustomWebApplicationFactory<Startup> _factory;

        public Create(CustomWebApplicationFactory<Startup> factory)
        {
            _factory = factory;
        }

        [Fact]
        public async Task GivenValidCreateTodoItemCommand_ReturnsSuccessCode()
        {
            var client = await _factory.GetAuthenticatedClientAsync();

            var command = new CreateTodoItemCommand
            {
                Title = "Do yet another thing."
            };

            var content = IntegrationTestHelper.GetRequestContent(command);

            var response = await client.PostAsync($"/api/todoitems", content);

            response.EnsureSuccessStatusCode();
        }
    ...

还有下面的阅读测试:

namespace CleanArchitecture.WebUI.IntegrationTests.Controllers.TodoItems
{
    public class Read: IClassFixture<CustomWebApplicationFactory<Startup>>
    {
        private readonly CustomWebApplicationFactory<Startup> _factory;

        public Read(CustomWebApplicationFactory<Startup> factory)
        {
            _factory = factory;
        }

        [Fact]
        public async Task ShouldRetriveAllTodos()
        {
            var client = await _factory.GetAuthenticatedClientAsync();

            var response = await client.GetAsync($"/api/todoitems");

            var todos = Deserialize(response);
            todos.Count().Should().Be(1); //but is 2 because database is shared between read and create test class.
        }

问题是内存数据库没有在每次测试之间重置。我尝试使用Guid.New.ToString 为内存数据库生成不同的名称,但随后测试找不到种子数据库数据,并且将测试放在同一个XUnit 集合中,但无济于事。

有什么好主意如何使测试不共享数据库?

【问题讨论】:

  • 您找到任何解决方案了吗?遇到同样的问题。

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


【解决方案1】:

对我有用的是为每个 WebApplicationFactory 实例生成一个 DBName,然后我为每个测试实例化其中一个。所以测试看起来像这样:

[Fact]
public void Test()
{
  // Arrange
  var appFactory = new WebApplicationFactory();
  // ...

  // Act
  // ...

  // Assert
  // ...
}

还有WebApplicationFactory

    public class TestWebApplicationFactory : WebApplicationFactory<Startup>
    {
        private readonly string _dbName = Guid.NewGuid().ToString();

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            base.ConfigureWebHost(builder);
            builder.ConfigureServices(services =>
            {
                services.AddDbContext<DbContext>(options =>
                {
                    // This is what makes a unique in-memory database per instance of TestWebApplicationFactory
                    options.UseInMemoryDatabase(_dbName);
                });
            });
        }
    }

【讨论】:

  • 我真的很好奇,您将数据库名称声明为字段的方法效果很好。然而,当我尝试在没有将数据库名称预先声明为字段或变量的情况下调用 UseInMemoryDatabase(Guid.NewGuid().ToString()) 时,我的测试失败了,为什么会发生这种情况?
  • 这不会创建一大堆内存数据库(即每个测试一个)吗?一旦你有几百/几千个测试用例会发生什么?
【解决方案2】:

在创建 DbContext 时,我们通过以下方式将测试相互隔离:

private static DbContextOptions<ApplicationDbContext> CreateDbContextOptions()
{
    return new DbContextOptionsBuilder<ApplicationDbContext>()
        //Database with same name gets reused, so let's isolate the tests from each other...
        .UseInMemoryDatabase(Guid.NewGuid().ToString()) 
        .Options;
}

在测试类中:

using (var context = new ApplicationDbContext(DbContextTestHelper.CreateDbContextOptions())
{
    //arrange data in context

    var testee = new XY(context);

    //do test

    //do asserts
}

【讨论】:

  • 如何隔离测试?在另一种方法中提取 DbContext 注册代码不会隔离任何内容。这段代码的作用是为每个 DbContext 实例生成一个 random 数据库名称。这可能是太孤立了。每个动作可能最终得到不同的数据集
  • @PanagiotisKanavos 是的,它为每个测试生成一个随机数据库名称
  • 但是如何将这个新生成的 DbContext 提供给 WebApplicationFactory?
  • @keuleJ 它为每个 context 生成一个新数据库。 DbContexts 是有作用域的,这意味着每个动作都有一个新动作。在同一个测试中调用两个动作可以从不同的数据库中提取数据
  • @TheFisherman 我们在测试中不使用依赖注入。我认为您必须以某种方式将此实例注册到范围。这样,您甚至可以将同一个数据库用于多个范围。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-08-11
  • 2014-05-24
  • 1970-01-01
  • 2016-06-19
  • 2015-08-08
  • 2017-10-20
  • 1970-01-01
相关资源
最近更新 更多