Unit testing with EF Core and in memory database
Solution 1
It looks like you might want a class fixture.
When to use: when you want to create a single test context and share it among all the tests in the class, and have it cleaned up after all the tests in the class have finished.
Create a separate class to setup whatever data your tests will share, and to clean it up when the tests are finished running.
public class MovieSeedDataFixture : IDisposable
{
public MovieDbContext MovieContext { get; private set; } = new MovieDbContext();
public MovieSeedDataFixture()
{
MovieContext.Movies.Add(new Movie { Id = 1, Title = "Movie 1", YearOfRelease = 2018, Genre = "Action" });
MovieContext.Movies.Add(new Movie { Id = 2, Title = "Movie 2", YearOfRelease = 2018, Genre = "Action" });
MovieContext.Movies.Add(new Movie { Id = 3, Title = "Movie 3", YearOfRelease = 2019, Genre = "Action" });
MovieContext.SaveChanges();
}
public void Dispose()
{
MovieContext.Dispose();
}
}
Then use it in your tests by extending the IClassFixture<T>
interface.
public class UnitTests : IClassFixture<MovieSeedDataFixture>
{
MovieSeedDataFixture fixture;
public UnitTests(MovieSeedDataFixture fixture)
{
this.fixture = fixture;
}
[Fact]
public void TestOne()
{
// use fixture.MovieContext in your tests
}
}
Solution 2
You can resolve the issue by appending the timestamp with the name of database name.
var myDatabaseName = "mydatabase_"+DateTime.Now.ToFileTimeUtc();
var options = new DbContextOptionsBuilder<BloggingContext>()
.UseInMemoryDatabase(databaseName: myDatabaseName )
.Options;
Only one database with given name is created in memory. (Documentation) Hence if you have same name this kind of exception may occur.
Similar discussion is there on this thread:
optionsBuilder.UseInMemoryDatabase("MyDatabase");
This creates/uses a database with the name “MyDatabase”. If UseInMemoryDatabase is called again with the same name, then the same in-memory database will be used, allowing it to be shared by multiple context instances.
And this github issue also suggests the same approach to add a unique string with database name Hope this helps.
Solution 3
Thanks, I did some changes in the fixture class and is working fine, even when I run both tests together.
Here is the change:
public class MovieSeedDataFixture : IDisposable
{
public MovieDbContext MovieContext { get; private set; }
public MovieSeedDataFixture()
{
var options = new DbContextOptionsBuilder<MovieDbContext>()
.UseInMemoryDatabase("MovieListDatabase")
.Options;
MovieContext = new MovieDbContext(options);
MovieContext.Movies.Add(new Movie { Id = 1, Title = "Movie 1", YearOfRelease = 2018, Genre = "Action" });
MovieContext.Movies.Add(new Movie { Id = 2, Title = "Movie 2", YearOfRelease = 2018, Genre = "Action" });
MovieContext.Movies.Add(new Movie { Id = 3, Title = "Movie 3", YearOfRelease = 2019, Genre = "Action" });
MovieContext.SaveChanges();
}
public void Dispose()
{
MovieContext.Dispose();
}
}
Solution 4
I just want to add additional solution for this discussion and mention a unique behavior in my test case.
The easiest way is to create a context factory and initiate it with a unique database name.
public static class ContextFactory
{
public static SampleContextCreateInMemoryContractContext()
{
var options = new DbContextOptionsBuilder<SchedulingContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new SampleContext(options);
}
}
Avoid using a static data when dealing with in memory context, in memory database context will try to mount all the data from the previous context even it has a different database name, weird :).
Solution 5
Since you are using XUnit then you can implement IDisposable
interface and drop database after all executions.
public void Dispose()
{
context.Database.EnsureDeleted();
context.Dispose();
}
For the developers who are working with NUnit, they can use a function with [TearDown]
attribute for same operation
Related videos on Youtube
MarcosF8
Updated on December 31, 2021Comments
-
MarcosF8 over 2 years
I am using ASP.NET Core 2.2, EF Core and MOQ. As you can see in the following code, I have two tests, and running both together, with both database name "MovieListDatabase" I got an error in one of the tests with this message:
Message: System.ArgumentException : An item with the same key has already been added. Key: 1
If I run each one separately they both pass.
And also, having a different database name in both tests, like "MovieListDatabase1" and "MovieListDatabase2" and running both together it pass again.
I have two questions: Why does this happen? and how can I refactor my code to re-use the in-memory database in both tests and make my test to look a bit cleaner?
public class MovieRepositoryTest { [Fact] public void GetAll_WhenCalled_ReturnsAllItems() { var options = new DbContextOptionsBuilder<MovieDbContext>() .UseInMemoryDatabase(databaseName: "MovieListDatabase") .Options; // Insert seed data into the database using one instance of the context using (var context = new MovieDbContext(options)) { context.Movies.Add(new Movie { Id = 1, Title = "Movie 1", YearOfRelease = 2018, Genre = "Action" }); context.Movies.Add(new Movie { Id = 2, Title = "Movie 2", YearOfRelease = 2018, Genre = "Action" }); context.Movies.Add(new Movie { Id = 3, Title = "Movie 3", YearOfRelease = 2019, Genre = "Action" }); context.SaveChanges(); } // Use a clean instance of the context to run the test using (var context = new MovieDbContext(options)) { var sut = new MovieRepository(context); //Act var movies = sut.GetAll(); //Assert Assert.Equal(3, movies.Count()); } } [Fact] public void Search_ValidTitlePassed_ReturnsOneMovie() { var filters = new MovieFilters { Title = "Movie 1", YearOfRelease = 2018, Genre = "Action" }; var options = new DbContextOptionsBuilder<MovieDbContext>() .UseInMemoryDatabase(databaseName: "MovieListDatabase") .Options; // Insert seed data into the database using one instance of the context using (var context = new MovieDbContext(options)) { context.Movies.Add(new Movie { Id = 1, Title = "Movie 1", YearOfRelease = 2018, Genre = "Action" }); context.Movies.Add(new Movie { Id = 2, Title = "Movie 2", YearOfRelease = 2018, Genre = "Action" }); context.Movies.Add(new Movie { Id = 3, Title = "Movie 3", YearOfRelease = 2019, Genre = "Action" }); context.SaveChanges(); } // Use a clean instance of the context to run the test using (var context = new MovieDbContext(options)) { var sut = new MovieRepository(context); //Act //var movies = _sut.Search(_filters); var movies = sut.Search(filters); //Assert Assert.Single(movies); } } }
And this is the repository class
public class MovieRepository: IMovieRepository { private readonly MovieDbContext _moviesDbContext; public MovieRepository(MovieDbContext moviesDbContext) { _moviesDbContext = moviesDbContext; } public IEnumerable<Movie> GetAll() { return _moviesDbContext.Movies; } public IEnumerable<Movie> Search(MovieFilters filters) { var title = filters.Title.ToLower(); var genre = filters.Genre.ToLower(); return _moviesDbContext.Movies.Where( p => (p.Title.Trim().ToLower().Contains(title) | string.IsNullOrWhiteSpace(p.Title)) & (p.Genre.Trim().ToLower().Contains(genre) | string.IsNullOrWhiteSpace(p.Genre)) & (p.YearOfRelease == filters.YearOfRelease | filters.YearOfRelease == null) ); } }
Thanks
-
MarcosF8 over 5 yearsThanks, I understood that. I am trying to see to how to refactor my code properly.
-
MarcosF8 over 5 yearsThe dbcontext is giving a big exeption, I think it is waiting for the options in the contrcutor:
-
Choco over 3 yearshow is the Constructor called with the instance of MovieSeedDataFixture?