119

I am trying to use the EF7 InMemory provider for unit tests, but the persistent nature of the InMemory database between tests is causing me problems.

The following code demonstrates my issue. One test will work and the other test will always fail. Even though I set the _context to null between tests, the second test run will always have 4 records in it.

[TestClass]
public class UnitTest1
{
    private SchoolContext _context;

    [TestInitialize]
    public void Setup()
    {
        Random rng = new Random();
        
        var optionsBuilder = new DbContextOptionsBuilder<SchoolContext>();
        optionsBuilder.UseInMemoryDatabase();

        _context = new SchoolContext(optionsBuilder.Options);
        _context.Students.AddRange(
            new Student { Id = rng.Next(1,10000), Name = "Able" },
            new Student { Id = rng.Next(1,10000), Name = "Bob" }
        );
        _context.SaveChanges();
    }

    [TestCleanup]
    public void Cleanup()
    {
        _context = null;
    }

    [TestMethod]
    public void TestMethod1()
    {
        Assert.AreEqual(2, _context.Students.ToList().Count());
    }

    [TestMethod]
    public void TestMethod2()
    {
        Assert.AreEqual(2, _context.Students.ToList().Count());
    }
}

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class SchoolContext : DbContext
{
    public SchoolContext(DbContextOptions options) : base(options) { }

    public DbSet<Student> Students { get; set; }
}
Callum Watkins
  • 2,844
  • 4
  • 29
  • 49
Sailing Judo
  • 11,083
  • 20
  • 66
  • 97

7 Answers7

162

The following call will clear the in-memory datastore.

_context.Database.EnsureDeleted();

Be careful that you'll never accidentally run this on a production database. According to the documentation:

Warning: The entire database is deleted and no effort is made to remove just the database objects that are used by the model for this context.

Bluebaron
  • 2,289
  • 2
  • 27
  • 37
natemcmaster
  • 25,673
  • 6
  • 78
  • 100
  • Thank you for this, it solved my problems. I initiall tried optionsBuilder.UseInMemoryDatabase(persist: false); which has been removed from EFCore and then stumbled across another possible solution for having different contexts between tests here: http://docs.efproject.net/en/latest/miscellaneous/testing.html I prefer the simplicity of the selected answer though for test root composition – Matt Sanders Feb 12 '16 at 22:50
  • 23
    This doesn't appear to reset the identify column of the in memory database. So if you are seeding the data with a row, the first test will see the row with id 1, the second test 2, etc. Is this by design? – ssmith Oct 26 '16 at 21:50
  • 5
    It's 2019 and the issue of the Id's persisting even after the database is dropped and is recreated is still an issue! – Tom Feb 05 '19 at 15:19
  • Nice. This fixed my problem! I thought that my error was that my tests were running in parallel but in reality it was that the in-memory database was not being cleared properly. – Thorkil Værge May 13 '19 at 16:05
  • I would suggest to use also R4nc1d answer below. You remove the database from memory AND also make sure you get a new database everytime with a fresh identify column. Just use `[TearDown]` (that runs after each test). – CularBytes Apr 13 '20 at 11:39
  • Sadly this does not work if the provider is SQLite-InMemory. – Muflix Feb 14 '22 at 12:50
  • @Muflix, I'm guessing there's a very good reason for that. It should only allow UseInMemoryDatabase to be able to do this so that you don't accidentally blow away your database records if you change that somehow. That's actually the question I came here to ask. Can anyone verify that I'm not in danger of blowing away live records if I do this. – Bluebaron Jan 31 '23 at 17:11
  • 1
    Warning: The entire database is deleted and no effort is made to remove just the database objects that are used by the model for this context. – Bluebaron Jan 31 '23 at 17:31
62

Bit late to the party, but i also ran into the same issue but what i ended up doing was.

Specifying a different database name for each test.

optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString());

That way you dont have to add

_context.Database.EnsureDeleted();

in all your tests

R4nc1d
  • 2,923
  • 3
  • 24
  • 44
  • 10
    Doesn't this still leave them in memory? – Sander Oct 17 '18 at 06:55
  • 4
    It will live in memory yes, but if you wrap your context in using statement it will automatically get disposed. – R4nc1d Mar 15 '19 at 13:06
  • 1
    Great addition to the top answer, this can be used to solve the identify column reset. I would still use `EnsureDeleted`, only need to add this once in the `[TearDown]` method that will run after each test so not much of a pain. – CularBytes Apr 13 '20 at 11:40
  • I'm new to unit testing with EF Core (using EF Core 5 now). I chose this approach for simplicity. I also have a small number of tests. By specifying a unique name with each test run, I'm concerned the impact of doing so when I've got hundreds of tests. Does anyone know about that? – Craig Boland Jan 14 '21 at 20:16
27

Simply change your code definition of DbContextOptionsBuilder to be like following :

        var databaseName = "DatabaseNameHere";
        var dbContextOption = new DbContextOptionsBuilder<SchoolContext>()
                                    .UseInMemoryDatabase(databaseName, new InMemoryDatabaseRoot())
                                    .Options;

new InMemoryDatabaseRoot() creates a new database without the issue of Id's persisting. So you don't need now for :

       [TestCleanup]
       public void Cleanup()
       {
           _context = null;
       }
Amin Mohamed
  • 640
  • 7
  • 10
  • 3
    +1 for the InMemoryDatabaseRoot. However, just using TestCleanup and setting the context to be null and re-creating a new context (assuming you use the same database name, and are not using InMemoryDatabaseRoot) in each TestInitialize will give you the same in-memory database. – bobwah Mar 27 '20 at 14:56
  • 1
    Nice, This solution resets the Identity column. – Varun Sharma Apr 13 '21 at 23:18
8

I would go with a combination of both answers. If tests run in parallel, you could have a database being deleted while you are in the middle of running another test, so I was seeing sporadic failures when running a 30+ tests.

Give it a random db name, and ensure it gets deleted when the test is completed.

public class MyRepositoryTests : IDisposable {
  private SchoolContext _context;

  [TestInitialize]
  public void Setup() {
    var options = new DbContextOptionsBuilder<ApplicationDbContext>()
      // Generate a random db name
      .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
      .Options;
      _context = new ApplicationDbContext(options);
  }

  [TestCleanup]
  public void Cleanup()
    _context.Database.EnsureDeleted(); // Remove from memory
    _context.Dispose();
  }
}
bradlis7
  • 3,375
  • 3
  • 26
  • 34
3

I use a DbContext fixture like the following

public class DbContextFixture 
    where TDbContext : DbContext
{
    private readonly DbContextOptions _dbContextOptions = 
        new DbContextOptionsBuilder()
            .UseInMemoryDatabase("_", new InMemoryDatabaseRoot())
            .Options;

    public TDbContext CreateDbContext()
    {
        return (TDbContext)(typeof(TDbContext)
            .GetConstructor(new[] { typeof(DbContextOptions) })
            .Invoke(new[] { _dbContextOptions }));
    }
}

you can now simply do

public class MyRepositoryTests : IDisposable {
    private SchoolContext _context;
    private DbContextFixture<ApplicationDbContext> _dbContextFixture;

    [TestInitialize]
    public void Setup() {
        _dbContextFixture = new DbContextFixture<ApplicationDbContext>();
        _context = _dbContextFixture.CreateDbContext();
        _context.Students.AddRange(
            new Student { Id = rng.Next(1,10000), Name = "Able" },
            new Student { Id = rng.Next(1,10000), Name = "Bob" }
        );
        _context.SaveChanges();
    }

    [TestCleanup]
    public void Cleanup()
        _context.Dispose();
        _dbContextFixture = null;
    }

    [TestMethod]
    public void TestMethod1()
    {
        Assert.AreEqual(2, _context.Students.ToList().Count());
    }

    [TestMethod]
    public void TestMethod2()
    {
        Assert.AreEqual(2, _context.Students.ToList().Count());
    }
}

This solution is thread-safe. See my blog for details.

0

The examples here achieve this through RemoveRange: https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.1

db.<Entity>.RemoveRange(db.<entity>);
Martin D
  • 160
  • 1
  • 12
0

Here is my 2 cent approach to keep each unit test isolated from each other. I am using C# 7, XUnit and EF core 3.1.

Sample TestFixture class.

public class SampleIntegrationTestFixture : IDisposable
    {

        public DbContextOptionsBuilder<SampleDbContext> SetupInMemoryDatabase()
            => new DbContextOptionsBuilder<SampleDbContext>().UseInMemoryDatabase("MyInMemoryDatabase");

 private IEnumerable<Student> CreateStudentStub()
            => new List<Student>
            {
            new Student { Id = rng.Next(1,10000), Name = "Able" },
            new Student { Id = rng.Next(1,10000), Name = "Bob" }
            };

        public void Dispose()
        {
        }
   }

Sample IntegrationTest class

 public class SampleJobIntegrationTest : IClassFixture<SampleIntegrationTestFixture >
 {
    private DbContextOptionsBuilder<SampleDbContext> DbContextBuilder { get; }
    private SampleDbContext SampleDbContext { get; set; }

 public SampleJobIntegrationTest(SampleIntegrationTestFixture 
 sampleIntegrationTestFixture )
  {
        SampleIntegrationTestFixture = sampleIntegrationTestFixture ;

        SampleDbContextBuilder = sampleIntegrationTestFixture .SetupInMemoryDatabase();
  }



  [Fact]
    public void TestMethod1()
    {
using(SampleDbContext = new SampleDbContext(SampleDbContextBuilder.Options))

        var students= SampleIntegrationTestFixture.CreateStudentStub();
            {
            SampleDbContext.Students.AddRange(students);

        SampleDbContext.SaveChanges();

  Assert.AreEqual(2, _context.Students.ToList().Count());

                SampleDbContext.Database.EnsureDeleted();
            }
      
    }
maxspan
  • 13,326
  • 15
  • 75
  • 104