46

I am trying use InMemory EF7 database for my xunit repository test.

But my problem is that when i try to Dispose the created context the in memory db persist. It means that one test involve other.

I have read this article Unit Testing Entity Framework 7 with the In Memory Data Store and I have tried to setup the context in the constructor of my TestClass. But this approach doesn't work. When I run tests separately everything is OK, but my first test method add something into DB and second test method start with dirty DB from previous test method. I try add IDispose into test class but method DatabaseContext and DB persist in memory. What I am doing wrong am i missing something?

My code looks like:

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);
        }


    }
}

I am using:

"Microsoft.EntityFrameworkCore.InMemory": "1.0.0"

"Microsoft.EntityFrameworkCore": "1.0.0"

"xunit": "2.2.0-beta2-build3300"
Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
Ivan Mjartan
  • 1,125
  • 1
  • 12
  • 23

2 Answers2

54

From the documentation,

Typically, EF creates a single IServiceProvider for all contexts of a given type in an AppDomain - meaning all context instances share the same InMemory database instance. By allowing one to be passed in, you can control the scope of the InMemory database.

Instead of making the test class disposable and trying to dispose the data context that way, create a new one for each test:

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;
}

Then, in each test, new up a data context using this method:

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

This approach should get you a squeaky-clean in-memory database for each test.

Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
  • Hallo Nate! Thank you very much! It is working! You save my a**! – Ivan Mjartan Aug 11 '16 at 15:48
  • @IvanMjartan Happy to help! – Nate Barbettini Aug 11 '16 at 16:01
  • Yes it looks great ... but I am disappointed because that simple constraint like not null etc is not working :( I think that there is not workaround or is there something? – Ivan Mjartan Aug 11 '16 at 16:44
  • @IvanMjartan I'm not quite sure. I'd recommend posting a new question. – Nate Barbettini Aug 12 '16 at 11:52
  • 1
    Hi Nate in last version constraints have been removed. But anyway I am using validation System.ComponentModel.DataAnnotations in my repositories before SvaeChanges and it works for me. :-) – Ivan Mjartan Aug 12 '16 at 12:00
  • 1
    Saved my life. Thank you! – starmandeluxe May 09 '17 at 00:17
  • 3
    @NateBarbettini Amazing, this works perfectly! But I'm still confused why this is really required. How is it not enough to specify a new database name each time? I'm using `.UseInMemoryDatabase(Guid.NewGuid().ToString())`, but still things get shared somehow. – MEMark Jun 06 '17 at 21:29
  • @MEMark I'm not really sure. I assumed like you did, but it seems like the in-memory database acts like a singleton in the background. Maybe there's a good explanation for it, but I'm not sure. – Nate Barbettini Jun 07 '17 at 05:22
  • Does this resolve this case? => https://stackoverflow.com/questions/47335920/entity-framework-core-inmemory-database-tests-break-when-run-in-parallel?noredirect=1#comment81644494_47335920 – SuperJMN Nov 17 '17 at 09:08
  • 1
    @MEMark it's been awhile but have you seen this? https://github.com/aspnet/EntityFrameworkCore/issues/6872 – IEnjoyEatingVegetables Feb 05 '19 at 12:39
  • This no longer works in EFC 5 with a creepy error: `A call was made to 'ReplaceService', but Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to 'UseInternalServiceProvider', or build replacement services into the service provider before passing it to 'UseInternalServiceProvider'.` even after providing a database name. – Konstantin Konstantinov Jun 07 '21 at 22:17
17

I think the answer Nate gave may be out of date now or maybe I am doing something wrong. UseInMemoryDatabase() now requires a db name.

Below is what I ended up with. I added a line to create a unique db name. I removed the using statements in favor of using the constructor and dispose that are called once for each test case.

There are some debug lines in there from my testing.

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;
    }

Here is a very basic test case.

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

I also made 8 of test cases that attempted to delete the same device with the same id and each passed.

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
Jeremy Lange
  • 171
  • 1
  • 3
  • 1
    It working perfectly. If anyone will have some information what is lifetime of the database in memory it will be great to share! – Pawel Wujczyk Oct 08 '19 at 16:55
  • Does the unique Db gets saved in a temporary file ? I tried to close the process and reopen, it still persists the data. If we create a new file everytime would it be a good engineering practice ? – Soundararajan May 29 '20 at 01:02
  • 1
    Thanks for this, was easily the best and easiest solution after a lot of searching! – Avrohom Yisroel Jan 07 '21 at 15:48