Reputation: 2008
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
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
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:
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);
}
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
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
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
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
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
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