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

Mar*_*sF8 18 c# unit-testing entity-framework moq .net-core

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

Message: System.ArgumentException : An item with the same key has already 
been added. Key: 1
Run Code Online (Sandbox Code Playgroud)

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

而且,在两个测试中使用不同的数据库名称,例如“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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是存储库类

 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)
                                             );
    }
}
Run Code Online (Sandbox Code Playgroud)

谢谢

Gra*_*ICA 14

看起来您可能想要一个class fixture

何时使用:当您想要创建单个测试上下文并在类中的所有测试之间共享它时,并在类中的所有测试完成后将其清理干净。

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

然后通过扩展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

    }
}
Run Code Online (Sandbox Code Playgroud)


小智 7

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

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

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


            return new SampleContext(options);
        }
     }
Run Code Online (Sandbox Code Playgroud)

在内存上下文中处理时避免使用静态数据,内存数据库上下文将尝试挂载前一个上下文中的所有数据,即使它具有不同的数据库名称,很奇怪:)。


Man*_*ari 5

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

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

var options = new DbContextOptionsBuilder<BloggingContext>()
                .UseInMemoryDatabase(databaseName: myDatabaseName )
                .Options;
Run Code Online (Sandbox Code Playgroud)

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

这个线程上有类似的讨论:

optionsBuilder.UseInMemoryDatabase("MyDatabase"); 
Run Code Online (Sandbox Code Playgroud)

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

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


Mar*_*sF8 5

谢谢,我在夹具类中做了一些更改并且工作正常,即使我一起运行两个测试也是如此。

这是变化:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)