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

时间:2019-12-07 13:14:30

标签: asp.net entity-framework asp.net-core-mvc entity-framework-core integration-testing

我正在学习ASP.NET Core 3,并已构建了一个基本应用程序。我正在寻找运行集成测试来断言对从数据库中正确读取/写入控制器的调用。为了避免依赖实际数据库,我正在考虑使用EF Core的内存数据库。我一直在关注this文章作为我的主要指南。

我遇到的问题是我正在努力确保每个单独的集成测试都使用新的数据库上下文。

最初,我多次调用数据库种子方法时遇到错误(第二次及以后的调用未能添加重复的主键-本质上是使用相同的上下文)。

通过查看此处的各种博客,教程和其他问题,我通过使用唯一名称(使用Guid.NewGuid())实例化内存数据库来解决此问题。这个应该解决了我的问题。但是,这给了我一个不同的问题。在每次测试初始化​​时都正确调用了数据库种子方法,但是当我随后调用控制器操作时,依赖项注入实例化了 new 数据库上下文,这意味着不存在种子数据!

我似乎只能圈一次,只能调用一次种子数据,只能进行一次测试,或者有多个测试,但是没有种子数据!

我已经尝试了DbContext服务的作用域生存期,将其设置为瞬态/作用域/单例,但结果似乎没有什么不同。

我设法使其起作用的唯一方法是在seed方法中将对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 个答案:

答案 0 :(得分:0)

 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 thread

中Grant所说的Grants提供的建议。