ASP.NET MVC CORE Web 应用程序集成测试与 EF Core 内存数据库 - 每次测试的新数据库

Posted

技术标签:

【中文标题】ASP.NET MVC CORE Web 应用程序集成测试与 EF Core 内存数据库 - 每次测试的新数据库【英文标题】:ASP.NET MVC CORE web application integration tests with EF Core in-memory database - fresh database for each test 【发布时间】: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);
        
    

【问题讨论】:

【参考方案1】:
 options.UseInMemoryDatabase("InMemoryDbForTesting");

这将创建/使用名为“MyDatabase”的数据库。如果使用相同的名称再次调用 UseInMemoryDatabase,则将使用相同的内存数据库,允许多个上下文实例共享它。

所以当你重复添加相同Id的数据时,你会得到"An item with the same key has already been added. Key: 3b477978-f280-11e9-8490-a8667f2f93c4"这样的错误

您可以在初始化方法中添加判断:

 private static void InitializeDbForTests(CompetitionStatsContext db)
        
            if (!db.Teams.Any())
            
                db.Teams.Add(new Team
                
                    Id = new Guid("3b477978-f280-11e9-8490-a8667f2f93c4"),
                    Name = "Arsenal"
                );
            

            db.SaveChanges();
        

您也可以参考格兰特在this threadadios SE 中提供的建议

【讨论】:

谢谢。该建议将允许多个测试运行,但它们不会每个都使用一个隔离的数据库,所以我必须找到某种方式来排序测试以确保它们不会相互干扰,这并不容易可通过大量测试进行维护。

以上是关于ASP.NET MVC CORE Web 应用程序集成测试与 EF Core 内存数据库 - 每次测试的新数据库的主要内容,如果未能解决你的问题,请参考以下文章

我应该如何保护我的 Web 应用程序(ASP.Net Core 3.1 MVC)?

ASP.NET Core Web 应用程序系列- 在ASP.NET Core中使用Autofac替换自带DI进行构造函数和属性的批量依赖注入(MVC当中应用)

从 ASP.NET MVC 5 移植到 dotnet core 2:可能替代 System.Web.Mvc.HttpStatusCodeResult [重复]

ASP.NET Core 2.1 MVC - 无法设置从 Web api 响应接收到的 cookie

ASP.NET Core Web 应用程序系列- 在ASP.NET Core中使用Autofac替换自带DI进行构造函数和属性的批量依赖注入(MVC当中应用)

在 ASP.NET Core 3 MVC Web 应用程序中设置路由