【问题标题】:How to isolate EF InMemory database per XUnit test如何根据 XUnit 测试隔离 EF InMemory 数据库
【发布时间】:2016-12-17 19:59:40
【问题描述】:

我正在尝试使用 InMemory EF7 数据库进行 xunit 存储库测试。

但我的问题是,当我尝试处置创建的上下文时,内存数据库仍然存在。这意味着一项测试涉及另一项测试。

我已阅读这篇文章Unit Testing Entity Framework 7 with the In Memory Data Store,并尝试在我的 TestClass 的构造函数中设置上下文。但这种方法行不通。当我单独运行测试时,一切都很好,但是我的第一个测试方法在 DB 中添加了一些东西,第二个测试方法从以前的测试方法中的脏 DB 开始。我尝试将 IDispose 添加到测试类中,但方法 DatabaseContext 和 DB 保留在内存中。我做错了什么我错过了什么吗?

我的代码如下:

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Fabric.Tests.Repositories
{
    /// <summary>
    /// Test for TaskRepository 
    /// </summary>
    public class TaskRepositoryTests:IDisposable
    {

        private readonly DatabaseContext contextMemory;

        /// <summary>
        /// Constructor
        /// </summary>
        public TaskRepositoryTests()
        {
            var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>();
            optionsBuilder.UseInMemoryDatabase();
            contextMemory = new DatabaseContext(optionsBuilder.Options);

        }

        /// <summary>
        /// Dispose DB 
        /// </summary>
        public void Dispose()
        {
            //this has no effect 
            if (contextMemory != null)
            {                
                contextMemory.Dispose();
            }
        }


        /// <summary>
        /// Positive Test for ListByAssigneeId method  
        /// </summary>
        /// <returns></returns>       
        [Fact]
        public async Task TasksRepositoryListByAssigneeId()
        {
            // Arrange
            var assigneeId = Guid.NewGuid();
            var taskList = new List<TaskItem>();


            //AssigneeId != assigneeId 
            taskList.Add(new TaskItem()
            {
                AssigneeId = Guid.NewGuid(),
                CreatorId = Guid.NewGuid(),
                Description = "Descr 2",
                Done = false,
                Id = Guid.NewGuid(),
                Location = "Some location 2",
                Title = "Some title 2"
            });

            taskList.Add(new TaskItem()
            {
                AssigneeId = assigneeId,
                CreatorId = Guid.NewGuid(),
                Description = "Descr",
                Done = false,
                Id = Guid.NewGuid(),
                Location = "Some location",
                Title = "Some title"
            });

            taskList.Add(new TaskItem()
            {
                AssigneeId = assigneeId,
                CreatorId = Guid.NewGuid(),
                Description = "Descr 2",
                Done = false,
                Id = Guid.NewGuid(),
                Location = "Some location 2",
                Title = "Some title 2"
            });

            //AssigneeId != assigneeId 
            taskList.Add(new TaskItem()
            {
                AssigneeId = Guid.NewGuid(),
                CreatorId = Guid.NewGuid(),
                Description = "Descr 2",
                Done = false,
                Id = Guid.NewGuid(),
                Location = "Some location 2",
                Title = "Some title 2"
            });


            //set up inmemory DB            
            contextMemory.TaskItems.AddRange(taskList);

            //save context
            contextMemory.SaveChanges();

            // Act
            var repository = new TaskRepository(contextMemory);
            var result = await repository.ListByAssigneeIdAsync(assigneeId);

            // Assert
            Assert.NotNull(result.Count());

            foreach (var td in result)
            {
                Assert.Equal(assigneeId, td.AssigneeId);
            }

        }

        /// <summary>
        /// test for Add method  
        /// (Skip = "not able to clear DB context yet")
        /// </summary>
        /// <returns></returns>
        [Fact]
        public async Task TasksRepositoryAdd()
        {
            var item = new TaskData()
            {
                AssigneeId = Guid.NewGuid(),
                CreatorId = Guid.NewGuid(),
                Description = "Descr",
                Done = false,
                Location = "Location",
                Title = "Title"
            };


            // Act
            var repository = new TaskRepository(contextMemory);
            var result = await repository.Add(item);

            // Assert
            Assert.Equal(1, contextMemory.TaskItems.Count());
            Assert.NotNull(result.Id);

            var dbRes = contextMemory.TaskItems.Where(s => s.Id == result.Id).SingleOrDefault();
            Assert.NotNull(dbRes);
            Assert.Equal(result.Id, dbRes.Id);
        }


    }
}

我正在使用:

"Microsoft.EntityFrameworkCore.InMemory": "1.0.0"

"Microsoft.EntityFrameworkCore": "1.0.0"

"xunit": "2.2.0-beta2-build3300"

【问题讨论】:

  • 您可以为每个测试命名您的 dbs。在这里查看我的答案stackoverflow.com/questions/34925833/…
  • 您好,感谢您的回答,这可以工作......我会试试的!
  • 提示:要生成和填充TaskItems的TaskList,请查看GenFu,例如:var taskList = GenFu.GenFu.ListOf&lt;TaskItems&gt;(20);

标签: c# .net entity-framework-core .net-core in-memory-database


【解决方案1】:

来自documentation

通常,EF 为 AppDomain 中给定类型的所有上下文创建单个 IServiceProvider - 这意味着所有上下文实例共享相同的 InMemory 数据库实例。通过允许传入一个,您可以控制 InMemory 数据库的范围。

不要将测试类设为一次性并尝试以这种方式处理数据上下文,而是为每个测试创建一个新的:

private static DbContextOptions<BloggingContext> CreateNewContextOptions()
{
    // Create a fresh service provider, and therefore a fresh 
    // InMemory database instance.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance telling the context to use an
    // InMemory database and the new service provider.
    var builder = new DbContextOptionsBuilder<DatabaseContext>();
    builder.UseInMemoryDatabase()
           .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}

然后,在每个测试中,使用此方法新建一个数据上下文:

using (var context = new DatabaseContext(CreateNewContextOptions()))
{
    // Do all of your data access and assertions in here
}

这种方法应该为每个测试提供一个非常干净的内存数据库。

【讨论】:

  • 你好,内特!非常感谢!这是工作!你救了我的**!
  • Hi Nate 在上一版本的限制已被移除。但无论如何,我在 SvaeChanges 之前的存储库中使用了验证 System.ComponentModel.DataAnnotations,它对我有用。 :-)
  • 救了我的命。谢谢!
  • @NateBarbettini 太棒了,这完美!但我仍然很困惑为什么这真的是必需的。每次都指定一个新的数据库名称还不够吗?我正在使用.UseInMemoryDatabase(Guid.NewGuid().ToString()),但还是会以某种方式共享内容。
  • @MEMark 已经有一段时间了,但你看到了吗? github.com/aspnet/EntityFrameworkCore/issues/6872
【解决方案2】:

我认为 Nate 给出的答案现在可能已经过时,或者我做错了什么。 UseInMemoryDatabase() 现在需要一个数据库名称。

以下是我的最终结果。我添加了一行来创建一个唯一的数据库名称。 我删除了 using 语句,转而使用为每个测试用例调用一次的构造函数和 dispose。

我的测试中有一些调试行。

public class DeviceRepositoryTests : IClassFixture<DatabaseFixture>, IDisposable
{

    private readonly DeviceDbContext _dbContext;
    private readonly DeviceRepository _repository;

    private readonly ITestOutputHelper _output;
    DatabaseFixture _dbFixture;

    public DeviceRepositoryTests(DatabaseFixture dbFixture, ITestOutputHelper output)
    {
        this._dbFixture = dbFixture;
        this._output = output;

        var dbOptBuilder = GetDbOptionsBuilder();
        this._dbContext = new DeviceDbContext(dbOptBuilder.Options);
        this._repository = new DeviceRepository(_dbContext);

        DeviceDbContextSeed.EnsureSeedDataForContext(_dbContext);
        //_output.WriteLine($"Database: {_dbContext.Database.GetDbConnection().Database}\n" +
        _output.WriteLine($"" +
              $"Locations: {_dbContext.Locations.Count()} \n" +
              $"Devices: {_dbContext.Devices.Count()} \n" +
              $"Device Types: {_dbContext.DeviceTypes.Count()} \n\n");

        //_output.WriteLine(deviceDbContextToString(_dbContext));
    }

    public void Dispose()
    {
        _output.WriteLine($"" +
                          $"Locations: {_dbContext.Locations.Count()} \n" +
                          $"Devices: {_dbContext.Devices.Count()} \n" +
                          $"Device Types: {_dbContext.DeviceTypes.Count()} \n\n");
        _dbContext.Dispose();
    }

    private static DbContextOptionsBuilder<DeviceDbContext> GetDbOptionsBuilder()
    {

        // The key to keeping the databases unique and not shared is 
        // generating a unique db name for each.
        string dbName = Guid.NewGuid().ToString();

        // Create a fresh service provider, and therefore a fresh 
        // InMemory database instance.
        var serviceProvider = new ServiceCollection()
            .AddEntityFrameworkInMemoryDatabase()
            .BuildServiceProvider();

        // Create a new options instance telling the context to use an
        // InMemory database and the new service provider.
        var builder = new DbContextOptionsBuilder<DeviceDbContext>();
        builder.UseInMemoryDatabase(dbName)
               .UseInternalServiceProvider(serviceProvider);

        return builder;
    }

这是一个非常基本的测试用例。

[Fact]
public void LocationExists_True()
{
    Assert.True(_repository.LocationExists(_dbFixture.GoodLocationId));
}

我还做了 8 个测试用例,尝试删除具有相同 id 的同一设备,每个都通过了。

【讨论】:

  • 它运行良好。如果有人知道数据库在内存中的生命周期是什么,那将是很好的分享!
  • 唯一的 Db 是否保存在临时文件中?我试图关闭进程并重新打开,它仍然保留数据。如果我们每次都创建一个新文件,这会是一个好的工程实践吗?
  • 感谢您,经过大量搜索,这很容易成为最佳和最简单的解决方案!
猜你喜欢
  • 2017-10-19
  • 2021-09-22
  • 2021-04-20
  • 1970-01-01
  • 1970-01-01
  • 2021-12-17
  • 1970-01-01
  • 2019-01-25
  • 1970-01-01
相关资源
最近更新 更多