Cómo aislar la base de datos EF InMemory por prueba de XUnit

Estoy tratando de usar la base de datos InMemory EF7 para mi prueba del repository xunit.

Pero mi problema es que cuando bash desechar el contexto creado, en la memoria db persisten. Significa que una prueba involucra a otra.

He leído este artículo Unit Testing Entity Framework 7 con In Memory Data Store y he intentado configurar el contexto en el constructor de mi TestClass. Pero este enfoque no funciona. Cuando ejecuto las pruebas por separado, todo está bien, pero mi primer método de prueba agrega algo a la base de datos y el segundo método de prueba comienza con la base de datos sucia del método de prueba anterior. Intento agregar IDispose en la clase de prueba, pero el método DatabaseContext y DB persisten en la memoria. ¿Qué estoy haciendo mal, me estoy perdiendo algo?

Mi código se ve como:

 using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Xunit; namespace Fabric.Tests.Repositories { ///  /// Test for TaskRepository ///  public class TaskRepositoryTests:IDisposable { private readonly DatabaseContext contextMemory; ///  /// Constructor ///  public TaskRepositoryTests() { var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseInMemoryDatabase(); contextMemory = new DatabaseContext(optionsBuilder.Options); } ///  /// Dispose DB ///  public void Dispose() { //this has no effect if (contextMemory != null) { contextMemory.Dispose(); } } ///  /// Positive Test for ListByAssigneeId method ///  ///  [Fact] public async Task TasksRepositoryListByAssigneeId() { // Arrange var assigneeId = Guid.NewGuid(); var taskList = new List(); //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); } } ///  /// test for Add method /// (Skip = "not able to clear DB context yet") ///  ///  [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); } } } 

Estoy usando:

 "Microsoft.EntityFrameworkCore.InMemory": "1.0.0" "Microsoft.EntityFrameworkCore": "1.0.0" "xunit": "2.2.0-beta2-build3300" 

De la documentación ,

Normalmente, EF crea un único IServiceProvider para todos los contextos de un tipo dado en un dominio de aplicación, lo que significa que todas las instancias de contexto comparten la misma instancia de base de datos de InMemory. Al permitir que se pase uno, puede controlar el scope de la base de datos de InMemory.

En lugar de hacer que la clase de prueba sea desechable y tratar de disponer el contexto de datos de esa manera, cree una nueva para cada prueba:

 private static DbContextOptions 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(); builder.UseInMemoryDatabase() .UseInternalServiceProvider(serviceProvider); return builder.Options; } 

Luego, en cada prueba, agregue un contexto de datos usando este método:

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

Este enfoque debería proporcionarle una base de datos en memoria limpia para cada prueba.

Creo que la respuesta que dio Nate puede estar desactualizada ahora o quizás esté haciendo algo mal. UseInMemoryDatabase() ahora requiere un nombre de db.

A continuación es lo que terminé. Agregué una línea para crear un nombre único de db. Eliminé las declaraciones de uso a favor del uso del constructor y deseché las llamadas una vez para cada caso de prueba.

Hay algunas líneas de depuración allí desde mis pruebas.

 public class DeviceRepositoryTests : IClassFixture, 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 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(); builder.UseInMemoryDatabase(dbName) .UseInternalServiceProvider(serviceProvider); return builder; } 

Aquí hay un caso de prueba muy básico.

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

También hice 8 de los casos de prueba que intentaron eliminar el mismo dispositivo con la misma ID y cada uno pasado.