如何根据 XUnit 测试隔离 EF InMemory 数据库
Posted
技术标签:
【中文标题】如何根据 XUnit 测试隔离 EF InMemory 数据库【英文标题】:How to isolate EF InMemory database per XUnit test 【发布时间】: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。在这里查看我的答案***.com/questions/34925833/… 您好,感谢您的回答,这可以工作......我会试试的! 提示:要生成和填充TaskItems的TaskList,请查看GenFu,例如:var taskList = GenFu.GenFu.ListOf<TaskItems>(20);
【参考方案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 是否保存在临时文件中?我试图关闭进程并重新打开,它仍然保留数据。如果我们每次都创建一个新文件,这会是一个好的工程实践吗? 感谢您,经过大量搜索,这很容易成为最佳和最简单的解决方案!以上是关于如何根据 XUnit 测试隔离 EF InMemory 数据库的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Rider 中进行 xUnit 测试?我收到错误 CS0246
如何使用 FluentAssertions 在 XUnit 中测试 MediatR 处理程序
如何在 xUnit 测试项目(.NET Core)中完成集成测试后关闭 Resharper 测试运行器?
使用 xunit 在 webapi 应用程序上运行 dotnet 测试