模拟 EF 核心 dbcontext 和 dbset

Posted

技术标签:

【中文标题】模拟 EF 核心 dbcontext 和 dbset【英文标题】:Mocking EF core dbcontext and dbset 【发布时间】:2019-06-10 16:44:52 【问题描述】:

我正在使用 ASP.NET Core 2.2、EF Core 和 MOQ。当我运行测试时,我收到了这个错误:

消息: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;
    
  

这是我的数据库上下文,带有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;
    

【问题讨论】:

可以显示你要测试的方法吗? EF Core 2 有一个in memory provider,它不需要模拟上下文,使用起来更好。见here。 MovieDbContext 中的 Movies 属性必须定义为 virtual 才能正确模拟 @DavidG 是的!这就是为什么我要求他展示他的方法来测试! 谢谢,我更新了显示存储库代码的帖子 【参考方案1】:

我看到您在 MovieRepository 中使用了 EF 核心 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(nnew 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包如下:

安装包 Microsoft.EntityFrameworkCore.InMemory

更多详情:Testing with InMemory

【讨论】:

那不是单元测试。那就是集成测试。单元测试的重点是从单元测试中删除依赖项。所以使用 ImMemory 数据库只是另一个依赖项。 @MZawg 我们测试这部分是因为我们的服务依赖于 EF DbContext。实际上,该单元测试应该模拟或存根所有依赖项。并使测试服务功能没有任何依赖。单元测试的主要质量是速度。但是,如果您的服务具有像某些 DbContext 实现这样的 EF 依赖项,那么您会问自己,您将如何模拟或存根此依赖项。不错的选择是使用可以包装 DbContext 的存储库模式。但这里我们讨论的是 EF 处理测试的内置能力。 EF Core 内存数据库不适合根据docs.microsoft.com/en-us/ef/core/miscellaneous/testing/…进行单元测试 @Bulgur 这不一定是真的。您可以使用它,但是:“不要这样做来测试实际的数据库查询或更新。” @Bulgur 上述链接说明了关于模拟 DBContext 的内容:“但是,我们从不尝试模拟 DbContext 或 IQueryable。这样做很困难、麻烦且脆弱。不要这样做。”而是:“在对使用 DbContext 的东西进行单元测试时使用 EF 内存数据库。” + 321X 指出的内容。【参考方案2】:

为了节省您的时间,请尝试使用我的 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 版本开始支持 AutoMapper 从 1.1.0 版本开始支持 DbQuery 从 3.0.0 版本开始支持 EF Core 3.0

【讨论】:

我对此感到困惑。如何在单个 DbContext 上设置多个表? @Ristogod 您可以为 DbContext 中的不同集合返回不同的模拟对象。 这是否支持实体的关系方面,如外键和导航属性? @R.Titov 所以我相信如果我正在测试通用存储库代码(这是一个内部库),那么我可以使用它而不用担心 DB 的行为方式,因为我的目标是作为单位测试是测试通用存储库代码而不是数据库返回给我的内容!我说的对吗? @kuldeep 是的。使用真实数据库的测试比单元测试更集成【参考方案3】:

使用Moq.EntityFrameworkCore 包。

这很简单:

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

【讨论】:

【参考方案4】:

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

构建起订量(通用方法)

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

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 就像一个通用存储库,并且模拟非常容易。

【讨论】:

【参考方案5】:

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

正如 cmets 中有人指出的那样,您应该使用 EF 内置的 memory provider 进行测试。

【讨论】:

谢谢,我像 Virtual 一样更改了,但仍然出现另一个错误。

以上是关于模拟 EF 核心 dbcontext 和 dbset的主要内容,如果未能解决你的问题,请参考以下文章

如何在 EF Core 中实例化 DbContext

使用映射从 edmx 生成 dbcontext 对象

无法使用 DbContext 实体框架核心 SaveChanges()

EF DbContext 和 ObjectContext 转换

使用EF 4.1的DbContext的方法大全

EF Code First DBContext 和事务