使用EF Core和内存数据库进行单元测试

时间:2019-01-19 06:55:34

标签: c# entity-framework unit-testing .net-core moq

我正在使用ASP.NET Core 2.2,EF Core和MOQ。如您在以下代码中看到的,我有两个测试,并且两个数据库同时运行,并且两个数据库名称均为“ MovieListDatabase”,在其中一个测试中,此消息出现错误:

Message: System.ArgumentException : An item with the same key has already 
been added. Key: 1

如果我分别运行每个,它们都会通过。

而且,在两个测试中都有一个不同的数据库名称,例如“ MovieListDatabase1”和“ MovieListDatabase2”,并且一起运行,它们再次通过。

我有两个问题: 为什么会这样?以及如何重构代码以在两个测试中重新使用内存数据库并使我的测试看起来更简洁?

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

这是存储库类

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

谢谢

7 个答案:

答案 0 :(得分:2)

您可以通过在时间戳上附加数据库名称来解决该问题。

var myDatabaseName = "mydatabase_"+DateTime.Now.ToFileTimeUtc();

var options = new DbContextOptionsBuilder<BloggingContext>()
                .UseInMemoryDatabase(databaseName: myDatabaseName )
                .Options;

尽管我没有在文档中看到此内容,但似乎在内存中仅创建了一个具有给定名称的数据库。因此,如果您使用相同的名称,则可能会发生这种异常。

这里有类似的讨论on this thread

optionsBuilder.UseInMemoryDatabase("MyDatabase"); 
  

这将创建/使用名为“ MyDatabase”的数据库。如果   再次使用相同的名称然后相同的名称调用UseInMemoryDatabase   将使用内存数据库,从而允许多个数据库共享   上下文实例。

并且this github问题还建议使用相同的方法来添加具有数据库名称的唯一字符串 希望这会有所帮助。

答案 1 :(得分:2)

您似乎想要一个class fixture

  

使用时间::要创建单个测试上下文并在类中的所有测试之间共享它,并在类中的所有测试完成后将其清除。 / p>

创建一个单独的类,以设置测试将共享的任何数据,并在测试完成运行后将其清除。

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

然后通过扩展IClassFixture<T>界面在测试中使用它。

public class UnitTests : IClassFixture<MovieSeedDataFixture>
{
    MovieSeedDataFixture fixture;

    public UnitTests(MovieSeedDataFixture fixture)
    {
        this.fixture = fixture;
    }

    [Fact]
    public void TestOne()
    {
        // use fixture.MovieContext in your tests

    }
}

答案 2 :(得分:1)

谢谢,即使我同时运行两个测试,我也对Fixture类进行了一些更改并且运行良好。

这里是变化:

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

答案 3 :(得分:1)

我只想为此讨论添加其他解决方案,并在测试用例中提及一种独特的行为。

最简单的方法是创建一个上下文工厂,并使用唯一的数据库名称启动它。

   public static class ContextFactory
    {
        public static SampleContextCreateInMemoryContractContext()
        {
            var options = new DbContextOptionsBuilder<SchedulingContext>()
               .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
               .Options;


            return new SampleContext(options);
        }
     }

在内存上下文中处理时避免使用静态数据,在内存数据库上下文中,即使它具有不同的数据库名称(怪异:),也会尝试挂载来自先前上下文的所有数据。

答案 4 :(得分:1)

由于您使用的是XUnit,因此您可以实现IDisposable接口并在所有执行后删除数据库。

    public void Dispose()
    {
        context.Database.EnsureDeleted();
        context.Dispose();
    }

对于使用NUnit的开发人员,他们可以将具有[TearDown]属性的函数用于同一操作

答案 5 :(得分:0)

使用夹具类,该测试给出了一个大错误:

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:没有为此DbContext配置数据库提供程序。可以通过重写DbContext.OnConfiguring方法或在应用程序服务提供程序上使用AddDbContext来配置提供程序。如果使用AddDbContext,则还应确保DbContext类型在其构造函数中接受DbContextOptions对象,并将其传递给DbContext的基本构造函数。 ----以下构造函数参数没有匹配的灯具数据:MovieSeedDataFixture灯具

我已经创建了一个空的构造函数来使用夹具类,但是,我想它需要使用带有以下选项的构造函数:

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

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

    }

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

答案 6 :(得分:0)

我认为另一种方法是在每次测试后重建并清空内存数据库。此外,可以为所有测试类编写一次与构建数据库相关的样板代码。以下示例展示了一种方法:

基类

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
 }

基类完成所有的初始化。相同的内存数据库被重用,但它被清空以避免测试工作在其他测试修改的数据上。

唯一我不太喜欢的方面是实际测试类中的显式重置功能,但它非常短,而且代码无论如何都必须在类中的某个地方。