模拟 EF 核心 dbcontext 和 dbset
Mocking EF core dbcontext and dbset
我正在使用 ASP.NET Core 2.2、EF Core 和最小起订量。当我 运行 测试时出现此错误:
Message: System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member: 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;
}
}
您收到的错误是因为您需要将 dbcontext 上的电影 属性 声明为虚拟电影。
正如有人在评论中指出的那样,您应该使用 memory provider 内置的 EF 进行测试。
我看到您在 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.InMemory
nuget 包,如下所示:
Install-Package Microsoft.EntityFrameworkCore.InMemory
为了节省您的时间,请尝试使用我的 Moq/NSubstitute 扩展 MockQueryable:https://github.com/romantitov/MockQueryable
支持所有 Sync/Async 操作
//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
这是 R.Titov
在 ASP.NET 核心 3.1 中完成的答案的发展:
构建起订量(通用方法)
克隆数据以允许并行测试 运行 并防止测试访问另一个更改的数据。
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 就像一个存储库,模拟非常简单。
使用 Moq.EntityFrameworkCore 包。
就这么简单:
var myDbContextMock = new Mock<MyDbContext>();
IList<Entity> entities = new List<Entity>() { new Entity(), new Entity() };
myDbContextMock.Setup(x => x.Entities).ReturnsDbSet(entities);
我正在使用 ASP.NET Core 2.2、EF Core 和最小起订量。当我 运行 测试时出现此错误:
Message: System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member: 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;
}
}
您收到的错误是因为您需要将 dbcontext 上的电影 属性 声明为虚拟电影。
正如有人在评论中指出的那样,您应该使用 memory provider 内置的 EF 进行测试。
我看到您在 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.InMemory
nuget 包,如下所示:
Install-Package Microsoft.EntityFrameworkCore.InMemory
为了节省您的时间,请尝试使用我的 Moq/NSubstitute 扩展 MockQueryable:https://github.com/romantitov/MockQueryable 支持所有 Sync/Async 操作
//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
这是 R.Titov
在 ASP.NET 核心 3.1 中完成的答案的发展:
构建起订量(通用方法)
克隆数据以允许并行测试 运行 并防止测试访问另一个更改的数据。
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 就像一个存储库,模拟非常简单。
使用 Moq.EntityFrameworkCore 包。
就这么简单:
var myDbContextMock = new Mock<MyDbContext>();
IList<Entity> entities = new List<Entity>() { new Entity(), new Entity() };
myDbContextMock.Setup(x => x.Entities).ReturnsDbSet(entities);