如何在单元测试之间重置 EF7内存提供程序?

我试图使用 EF7 InMemory 提供程序进行单元测试,但是测试之间 InMemory 数据库的持久性给我带来了问题。

下面的代码演示了我的问题。一个测试会成功,而另一个测试总是会失败。即使我在两次测试之间将 _context设置为 null,第二次测试运行中始终有4条记录。

[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; }
}
25811 次浏览

The following call will clear the in-memory datastore.

_context.Database.EnsureDeleted();

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

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

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.

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

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

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

}