使用 ASP.NET Core 和 Entity Framework Core 进行集成测试 - 如何在每次测试时恢复数据库中的测试数据?

Posted

技术标签:

【中文标题】使用 ASP.NET Core 和 Entity Framework Core 进行集成测试 - 如何在每次测试时恢复数据库中的测试数据?【英文标题】:Integration Testing with ASP.NET Core and Entity Framework Core - How to Restore Test Data in Database Per Test? 【发布时间】:2020-06-16 00:47:33 【问题描述】:

我是新的 .NET Core 用户。我正在尝试对使用 EF Core 来管理数据的 ASP.NET.Core WebAPI 执行集成测试。架构基于eShopContainers DDD 示例。我正在使用 Xunit 进行测试,并考虑了三种方法将在 docker-compose 堆栈中运行的数据库重置为运行每个测试之前的初始状态:

    测试设置:使用测试数据创建和种子数据库,TearDown:删除数据库。 Test Setup:包含测试数据的种子数据库表,TearDown:清除测试数据。 Test Setup:创建事务,TearDown:中止事务。

被测系统使用 BeginTransaction() 在事务中执行创建/删除/更新操作。不确定 EF Core 是否可以使用嵌套事务,因此在测试时尝试执行选项 1 和 2。

下面的代码使用 WebApiTestFactory 作为派生自WebApplicationFactory 的类。这是作为类夹具提供的,用于创建用于测试的 TestServer 实例。

    public class CoursesScenarios : CoursesScenarioBase, IClassFixture<WebApiTestFactory>, IDisposable
    
        /// <summary> 
        /// With Xunit constructor gets run before every test
        /// Code in the Dispose method gets run after every test completed
        /// </summary>
        public CoursesScenarios(WebApiTestFactory factory) : base(factory)
        
            Console.WriteLine("CoursesScenario test setup...");

            Server.Host
                .MigrateDbContext<CourseContext>((context, services) =>
                
                    Console.WriteLine("Seeding lookup data");
                    var env = services.GetService<IWebHostEnvironment>();
                    var settings = services.GetService<IOptions<CourseSettings>>();
                    var logger = services.GetService<ILogger<CoursesContextSeed>>();

                    new CoursesContextSeed()
                        .SeedAsync(context, env, settings, logger)
                        .Wait();
                );

            // load the test data
            Server.Host
                .MigrateDbContext<CourseContext>((context, services) =>
                
                    Console.WriteLine("Seeding the test data");
                    var env = services.GetService<IWebHostEnvironment>();
                    var settings = services.GetService<IOptions<CourseSettings>>();
                    var logger = services.GetService<ILogger<CoursesContextTestSeed>>();

                    new CoursesContextTestSeed()
                        .SeedAsync(context, env, settings, logger)
                        .Wait();
                );

            foreach(Course c in Context.Courses)
            
                Console.WriteLine($"Course c.Id");
            

        

        // tests here
        [Fact]
        ...
        //

        ///
        /// <summary>
        /// This gets called after every test by XUnit
        /// </summary>
        ///
        public void Dispose()
        
            // using (var command = Context.Database.GetDbConnection().CreateCommand())
            // 
            //     command.CommandText = @"DELETE FROM CourseManagement.Course;
            //         ALTER SEQUENCE coursemanagement.""unit_UnitID_seq"" RESTART WITH 1;";
            //     Context.Database.OpenConnection();
            //     int result = command.ExecuteNonQuery();
            //     Console.WriteLine($"result records deleted from course table");
            //     how to reload entities to match deleted state of rows in table?
            //     currently entities in memory are saved back to database
            // 
            Context.Database.EnsureDeleted();
        

以下异常在服务器上引发并在测试期间收到。这是由于删除了数据库,所以连接丢失了。

Npgsql.NpgsqlException (0x80004005): Exception while connecting
 ---> System.Net.Sockets.SocketException (111): Connection refused
   at Npgsql.NpgsqlConnector.Connect(NpgsqlTimeout timeout)
   at Npgsql.NpgsqlConnector.Connect(NpgsqlTimeout timeout)
   at Npgsql.NpgsqlConnector.RawOpen(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
   at Npgsql.NpgsqlConnector.Open(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
   at Npgsql.NpgsqlConnection.<>c__DisplayClass32_0.<<Open>g__OpenLong|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Npgsql.NpgsqlConnection.Open()
   at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlDatabaseCreator.Exists()
   at Microsoft.EntityFrameworkCore.Migrations.HistoryRepository.Exists()
   at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.Migrate(String targetMigration)
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.Migrate(DatabaseFacade databaseFacade)
   at Microsoft.AspNetCore.Hosting.IWebHostExtensions.InvokeSeeder[TContext](Action`2 seeder, TContext context, IServiceProvider services) in /src/BuildingBlocks/WebHost.Customization/IWebHostExtensions.cs:line 76
   at Microsoft.AspNetCore.Hosting.IWebHostExtensions.<>c__DisplayClass1_0`1.<MigrateDbContext>b__2() in /src/BuildingBlocks/WebHost.Customization/IWebHostExtensions.cs:line 54
   at Polly.Policy.<>c__DisplayClass108_0.<Execute>b__0(Context ctx, CancellationToken ct)
   at Polly.Policy.<>c__DisplayClass138_0.<Implementation>b__0(Context ctx, CancellationToken token)
   at Polly.Retry.RetryEngine.Implementation[TResult](Func`3 action, Context context, CancellationToken cancellationToken, ExceptionPredicates shouldRetryExceptionPredicates, ResultPredicates`1 shouldRetryResultPredicates, Action`4 onRetry, Int32 permittedRetryCount, IEnumerable`1 sleepDurationsEnumerable, Func`4 sleepDurationProvider)

是否有人设法使用 EF Core 成功重置数据库状态以进行集成测试,无论是使用删除数据库方法还是种子/取消种子表状态?还尝试了种子数据/非种子数据方法,重置身份密钥并使用原始 SQL 查询删除记录,但随后在将 EF Core 与基础表中已删除行的状态重新同步时遇到了困难,EF Core 将它们视为新实体并添加他们回来了。也许最好的方法是使用测试数据 sql 脚本和 ADO.NET 来初始化测试数据状态?

【问题讨论】:

请查看github.com/v-zubritsky/Reseed 是否有帮助 【参考方案1】:

某些操作,您可能希望在整个测试运行中执行一次(即数据库创建和删除)。

为此,您可以使用ICollectionFixture&lt;T&gt;

public class DatabaseFixture : IDisposable

    public DatabaseFixture()
    
        // ... initialize database and sample data
        DbContext = ...
        DbContext.Database.Migrate();
    

    public void Dispose()
    
        // ... clean up test data from the database ...
        DbContext.Database.EnsureDeleted();
        DbContext.Dispose();
    

    public DbContext DbContext  get; private set; 


[CollectionDefinition(nameof(DatabaseCollection))]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>

    // This class has no code, and is never created. Its purpose is simply
    // to be the place to apply [CollectionDefinition] and all the
    // ICollectionFixture<> interfaces.


[Collection(nameof(DatabaseCollection))]
public class DatabaseTestClass1

    DatabaseFixture fixture;

    public DatabaseTestClass1(DatabaseFixture fixture)
    
        this.fixture = fixture;
    


[Collection(nameof(DatabaseCollection))]
public class DatabaseTestClass2

    // ...

【讨论】:

【参考方案2】:

已解决,通过使用每个测试的测试设置/拆卸而不是类夹具并重新启动表中 postgresql 的新默认标识列上的标识,如建议的 here。

using (context)
 
     // could execute a DELETE statement via command on the course and units
    // to improve performance, otherwise a DELETE command is issued for each entity?
     context.Courses.RemoveRange(context.Courses);
     context.Units.RemoveRange(context.Units);
     await context.SaveChangesAsync();

     using (var command = context.Database.GetDbConnection().CreateCommand())
     
         command.CommandText = @"ALTER TABLE CourseManagement.Unit ALTER COLUMN ""UnitID"" RESTART WITH 1";
         context.Database.OpenConnection();
         await command.ExecuteNonQueryAsync();
     

     _logger.LogInformation($"------ nameof(TestSeed) deseeded test data");

【讨论】:

以上是关于使用 ASP.NET Core 和 Entity Framework Core 进行集成测试 - 如何在每次测试时恢复数据库中的测试数据?的主要内容,如果未能解决你的问题,请参考以下文章

使用 ASP.NET Core, Entity Framework Core 和 ABP 创建N层Web应用 第二篇

这是如何使用 Entity Framework Core 和 ASP.NET Core MVC 2.2+ 和 3.0 创建数据传输对象 (DTO)

使用 ASP.NET Core 和 Entity Framework Core 进行集成测试 - 如何在每次测试时恢复数据库中的测试数据?

在 ASP.NET Core 中使用 Entity Framework 6

根据 ASP.NET Core 和 Entity Framework Core 中的条件禁用 [必需] 属性

C# ASP.NET Core Entity Framework Core 异步 ToQueryable 比较