模拟 EF Core DbContext 和 DbSet

93
我正在使用ASP.NET Core 2.2,EF Core和MOQ。当我运行测试时,我遇到了这个错误:
Message: System.NotSupportedException : 对非虚拟成员(在VB中可以重写)进行无效设置:x => x.Movies
我做错了什么?
public class MovieRepositoryTest
{
    private readonly MovieRepository _sut;

    public MovieRepositoryTest()
    {
        var moviesMock = CreateDbSetMock(GetFakeListOfMovies());
        var mockDbContext = new Mock<MovieDbContext>();
        mockDbContext.Setup(x => x.Movies).Returns(moviesMock.Object);
        _sut = new MovieRepository(mockDbContext.Object);
    }

    [Fact]
    public void GetAll_WhenCalled_ReturnsAllItems()
    {
        //Act
        var items = _sut.GetAll();

        //Assert
        Assert.Equal(3, items.Count());
    }

    private IEnumerable<Movie> GetFakeListOfMovies()
    {
        var movies = new List<Movie>
        {
            new Movie {Id = 1, Title = "Movie 1", YearOfRelease = 2018, Genre = "Action"},
            new Movie {Id = 2, Title = "Movie 2", YearOfRelease = 2018, Genre = "Action"},
            new Movie {Id = 3, Title = "Movie 3", YearOfRelease = 2019, Genre = "Action"}
        };

        return movies;
    }

    private static Mock<DbSet<T>> CreateDbSetMock<T>(IEnumerable<T> elements) where T : class
    {
        var elementsAsQueryable = elements.AsQueryable();
        var dbSetMock = new Mock<DbSet<T>>();

        dbSetMock.As<IQueryable<T>>().Setup(m => m.Provider).Returns(elementsAsQueryable.Provider);
        dbSetMock.As<IQueryable<T>>().Setup(m => m.Expression).Returns(elementsAsQueryable.Expression);
        dbSetMock.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(elementsAsQueryable.ElementType);
        dbSetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(elementsAsQueryable.GetEnumerator());

        return dbSetMock;
    }
  }

这是我的DB上下文,其中包含Movie dbSet:

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

    }

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

需要测试的方法是带有GetAll的存储库:

 public class MovieRepository: IMovieRepository
{
    private readonly MovieDbContext _moviesDbContext;
    public MovieRepository(MovieDbContext moviesDbContext)
    {
        _moviesDbContext = moviesDbContext;
    }

    public IEnumerable<Movie> GetAll()
    {
        return _moviesDbContext.Movies;
    }
}

你能展示一下你要测试的方法吗? - TanvirArjel
1
EF Core 2拥有一个内存提供程序,它消除了模拟上下文的需要,使用起来更加舒适。请参见此处 - DavidG
3
MovieDbContext 中,Movies 属性必须定义为 virtual 才能正确模拟。 - haim770
@DavidG 是的!这就是为什么我要求他展示他的方法以进行测试的原因! - TanvirArjel
谢谢,我更新了帖子并展示了代码库的代码。 - MarcosF8
显示剩余2条评论
6个回答

185

我看到您在 MovieRepository 中使用了 EF Core 的 DbContext。因此,与其使用模拟对象,使用 EF Core 的 InMemory 数据库会是一个更好的选择。这还可以减少复杂性。

将您的 GetAllTest() 方法编写如下:

[Fact]
public void GetAllTest()
{
        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))
        {
            MovieRepository movieRepository = new MovieRepository(context);
            List<Movies> movies == movieRepository.GetAll();

            Assert.Equal(3, movies.Count);
        }
}

注意:不要忘记按以下步骤安装Microsoft.EntityFrameworkCore.InMemorynuget包:

Install-Package Microsoft.EntityFrameworkCore.InMemory

更多详情请参见:使用InMemory测试


66
那不是单元测试,而是集成测试。单元测试的整个重点在于从单元测试中删除依赖项。因此,使用内存数据库只是另一个依赖项。 - Alexander Pavlenko
8
我们测试这个部分是因为我们的服务依赖于EF DbContext。实际上,该单元测试应该模拟或存根所有依赖项,并使测试服务功能不受任何依赖项的影响。单元测试的主要质量是速度。但是,如果您的服务像某些DbContext实现一样依赖于EF,则需要考虑如何模拟或存根此依赖关系。好的选择是使用存储库模式来包装DbContext。但在这里,我们谈论的是EF内置处理测试的能力。 - Alexander Pavlenko
5
根据 https://learn.microsoft.com/en-us/ef/core/miscellaneous/testing/#approach-3-the-ef-core-in-memory-database,EF Core 内存数据库不适合用于单元测试。 - Bulgur
10
@Bulgur 这并不一定是正确的。你可以使用它,但是:"只是不要将其用于测试实际数据库查询或更新。" - 321X
28
上述链接提到如下关于模拟 DbContext 的内容:“然而,我们从不尝试模拟 DbContext 或 IQueryable。这样做很困难、繁琐且容易出错。不要这样做。”相反地,应该使用 EF 内存数据库来进行单元测试,当需要使用 DbContext 时。同时注意参考 321X 的建议。 - Rohin Tak
显示剩余6条评论

32
使用Moq.EntityFrameworkCore包很简单,只需要:
using Moq.EntityFrameworkCore;

var myDbContextMock = new Mock<MyDbContext>();
var entities = new List<Entity>() { new Entity(), new Entity() };
myDbContextMock.Setup(x => x.Entities).ReturnsDbSet(entities);

1
如果我只在文件顶部包含 using Moq.EntityFrameworkCore;,编译器无法识别 Mock<> 类。我还必须包括 using Moq;,然后您的解决方案就不起作用了。但是在 repo 的示例测试文件 中,他们似乎不需要引用 Moq,只需要引用 Moq.EntityFrameworkCore? - bongoSLAP
你说得对,他们使用了 var userContextMock = new Mock<UsersContext>();,但是他们没有使用 using Moq;。然而,Moq包在他们的项目文件中被引用,所以我认为它已经被某种方式包含进去了。我没有尝试过他们的示例。无论如何,我已经成功地在自己的项目中使用了Moq.EntityFrameworkCore库,使用了我发布的代码的改编版本,遵循了官方的“使用”文档。 - Kolazomai
Moq.EntityFrameworkCore 实际上只支持 .Net6.0,不支持 core :( 如果它能在 .net core 上运行就太棒了,可惜不行 :( - Dan Rayson
3
请使用此软件包的先前版本。 - Delonous
1
我认为这是一个很好的解决方案,适用于那些选择不使用EF Core仓储模式的人。它简单而优雅。 - jarodsmk
我对这种问题有许多不同的解决方案,因为在不同的项目中,DBContext的实现方式有很大的差异。我以前从未遇到过这种情况。它完全符合我的要求。 - undefined

30

为了节省您的时间,请尝试使用我的Moq/NSubstitute扩展MockQueryable:https://github.com/romantitov/MockQueryable 支持所有同步/异步操作

//1 - create a List<T> with test items
var users = new List<UserEntity>()
{
 new UserEntity,
 ...
};

//2 - build mock by extension
var mock = users.AsQueryable().BuildMock();

//3 - setup the mock as Queryable for Moq
_userRepository.Setup(x => x.GetQueryable()).Returns(mock.Object);

//3 - setup the mock as Queryable for NSubstitute
_userRepository.GetQueryable().Returns(mock);

DbSet也得到了支持

//2 - build mock by extension
var mock = users.AsQueryable().BuildMockDbSet();

//3 - setup DbSet for Moq
var userRepository = new TestDbSetRepository(mock.Object);

//3 - setup DbSet for NSubstitute
var userRepository = new TestDbSetRepository(mock);

注意:

  • 自动映射工具支持1.0.4版本及以上。
  • DbQuery支持1.1.0版本及以上。
  • EF Core 3.0支持3.0.0版本及以上。

我对此感到困惑。您如何在单个DbContext上设置多个表? - Ristogod
@Ristogod 你可以为 DbContext 中的不同集合返回不同的模拟对象。 - R.Titov
2
@R.Titov,我认为如果我正在测试一个通用的存储库代码(这是一个内部库),那么我可以放心地使用它,而不必担心数据库的行为,因为我的单元测试目标是测试通用存储库代码,而不是数据库返回给我的内容!我正确吗? - kuldeep
1
@kuldeep 是的。使用真实数据库进行测试比单元测试更多涉及到集成测试。 - R.Titov
1
@krillgar,请查看来自 https://github.com/romantitov/MockQueryable/blob/master/src/MockQueryable/MockQueryable.Sample/MyServiceMoqTests.cs 的测试DbSetFindAsyncUserEntity()。 - R.Titov
显示剩余6条评论

3
这是在ASP.NET Core 3.1中完成的R.Titov答案的开发:

构建Moq(通用方法)

数据被克隆以允许测试并行运行,并防止一个测试访问另一个更改的数据。

public static Mock<DbSet<TEnt>> SetDbSetData<TEnt>(this Mock<IApplicationDbContext> dbMock,
        IList<TEnt> list, bool clone = true) 
    where TEnt : class
{
    var clonedList = clone ? list.DeepClone().ToList() : list.ToList();
    var mockDbSet = clonedList.AsQueryable().BuildMockDbSet();

    dbMock.Setup(m => m.Set<TEnt>()).Returns(mockDbSet.Object);
    dbMock.Setup(m => m.ReadSet<TEnt>()).Returns(mockDbSet.Object.AsQueryable());

    return mockDbSet;
}

使用一些测试数据
_appUserDbSetMock = _dbMock.SetDbSetData(ApplicationUserTestData.ApplicationUserData);

示例测试

[Fact]
private async Task Handle_ShouldAddANewUser()
{
    var command = new CreateApplicationUserCommand
    {
        // ...
    };

    await _handler.Handle(command, default);

    _appUserDbSetMock.Verify(m => m.AddAsync(It.IsAny<ApplicationUser>(), default), Times.Once);
}

使用 MoqQueryable 的一个优点是不需要泛型仓库,因为 DbSet 就像一个泛型仓库一样,并且模拟非常容易。

0

你收到的错误是因为你需要在你的dbcontext上声明Movies属性为Virtual。

正如某人在评论中指出的那样,你应该使用EF内置的内存提供程序进行测试。


谢谢,我已经像虚拟机一样更改了,但仍然出现另一个错误。 - MarcosF8

-1

为单元测试项目设置依赖注入(.NET Core 5 和 xUnit 2.4)

1. add a startup.cs file with a class Startup 
2.  public void ConfigureServices(IServiceCollection services)
    {
        var configuration = new ConfigurationBuilder()
      .SetBasePath(System.IO.Directory.GetCurrentDirectory())
      .AddJsonFile("appsettings.Development.json", false, true)
      .Build();
        //setups nlog dependency injection

        services.AddControllers();

        var connectionstring = configuration.GetConnectionString("DbCoreConnectionString");

        services.AddDbContext<ViewpointContext>(options1 => options1.UseSqlServer(connectionString));

        services.AddScoped<IRepositoryDB, RepositoryDB>();

        services.ConfigureLoggerService();

    }

 3. in your xunit class add your dependency injection
      IRepositoryDB _db;
      public TestSuite(ITestOutputHelper output,IRepositoryDB db)
    {
         _db=db;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接