Unit testing with EF Core and in memory database

34,240

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

Share:
34,240

Related videos on Youtube

MarcosF8
Author by

MarcosF8

Updated on December 31, 2021

Comments

  • MarcosF8
    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
    MarcosF8 over 5 years
    Thanks, I understood that. I am trying to see to how to refactor my code properly.
  • MarcosF8
    MarcosF8 over 5 years
    The dbcontext is giving a big exeption, I think it is waiting for the options in the contrcutor:
  • Choco
    Choco over 3 years
    how is the Constructor called with the instance of MovieSeedDataFixture?