MarcosF8
MarcosF8

Reputation: 2008

Unit testing with EF Core and in memory database

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

Upvotes: 30

Views: 73543

Answers (7)

Manoj Choudhari
Manoj Choudhari

Reputation: 5634

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.

Upvotes: 12

Alexei - check Codidact
Alexei - check Codidact

Reputation: 23098

I think another approach is to reconstruct and empty the in-memory database after each test. Also, the boiler-plate code related to constructing the database can be written once for all test classes. The following example shows one way to do it:

Base class

public abstract class InMemoryTestBase
{
    protected IApplicationDbContext DbContext { get; private set; }

    protected InMemoryTestBase()
    {
        Init();
    }

    protected abstract void Reset();

    private void Init()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase("ApplicationDbContext")
            .Options;

        DbContext = new ApplicationDbContext(options);

        Populate();
        DbContext.SaveChanges();

        Reset();
    }

    private void Populate()
    {
        DbContext.EnsureDeleted();

        PopulateApplicationUserData();
    }

    private void PopulateApplicationUserData()
    {
        DbContext.Set<ApplicationUser>().AddRange(ApplicationUserTestData.ApplicationUserData);
        DbContext.Set<ApplicationUserRole>().AddRange(ApplicationUserTestData.ApplicationUserRoleData);
    }

An example test class

public class GetApplicationUserCountQueryHandlerTests : InMemoryTestBase
{
    private IRequestHandler<GetApplicationUserCountQuery, int> _handler;
    
    protected override void Reset()
    {
        _handler = new GetApplicationUserCountQueryHandler(DbContext);
    }


    [Fact]
    public async Task Handle_ShouldReturnAllUserCountIfFilteringNonArchived()
    {
        int count = await _handler.Handle(new GetApplicationUserCountQuery, default);

        count.Should().Be(ApplicationUserTestData.ApplicationUserData.Count);
    }

    // other tests come here
 }

The base class does all the initialization. The same in-memory database is reused, but it is emptied to avoid tests working on data modified by other tests.

The only aspect I do not particularly like is that the explicit Reset functionality in the actual test class, but it is very short and that code must be somewhere in the class anyway.

Upvotes: 2

Can PERK
Can PERK

Reputation: 630

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

Upvotes: 2

Jend DimShu
Jend DimShu

Reputation: 97

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 :).

Upvotes: 7

MarcosF8
MarcosF8

Reputation: 2008

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

Upvotes: 7

MarcosF8
MarcosF8

Reputation: 2008

The test is giving a big error using the fixture class:

Message: System.AggregateException : One or more errors occurred. (No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.) (The following constructor parameters did not have matching fixture data: MovieSeedDataFixture fixture)

---- System.InvalidOperationException : No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext. ---- The following constructor parameters did not have matching fixture data: MovieSeedDataFixture fixture

I Have created an empty constructor to use the fixture class but, I guess it need to use the constructor with the options:

public class MovieDbContext: DbContext
{
    public MovieDbContext()
    {
    }

    public MovieDbContext(DbContextOptions<MovieDbContext> options) : base(options)
    {

    }

    public DbSet<Movie> Movies { get; set; }
}

Upvotes: 0

Grant Winney
Grant Winney

Reputation: 66499

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

    }
}

Upvotes: 24

Related Questions