如何正确地对与 Entity Framework / DbContext 耦合的存储库模式方法进行单元测试?
How do I properly unit test a repository-pattern method, that has coupling to Entity Framework / DbContext?
我是单元测试的新手,希望编写一个 N 层的应用程序,因此我的 ORM 是从 BLL 中抽象出来的,中间有一个存储库模式。我将如何对与 EF (DbContext) 耦合的存储库模式方法进行单元测试?
我的SUT方法在这里:
public IList<Volunteer> GetAllVolunteers() {
return dbctx.Volunteer.ToList();
}
到目前为止我的单元测试:
[Fact]
public void GetAllVolunteersMethodWorks() {
// Arrange
var fakeVolunteer = new Volunteer { Id = 0, Name = "Dummy" };
var expected = new List<Volunteer>(new Volunteer[] { fakeVolunteer });
var mockedCtxVolunteer = new Mock<DbSet<Volunteer>>();
mockedCtxVolunteer.Setup(m => m.ToList()).Returns(expected);
var mockedctx = new Mock<RSMContext>();
mockedctx.Setup(m => m.Volunteer).Returns(mockedCtxVolunteer.Object);
// Act
var volunteerRepo = new VolunteerRepository(mockedctx.Object);
var result = volunteerRepo.GetAllVolunteers();
// Assert
Assert.Equal(expected, result);
}
我得到一个错误 System.NotSupportedException:'表达式引用了一个不属于模拟对象的方法:m => m.ToList()
那么我该如何正确地模拟 DbSet,以便我的假数据驻留在那里以正确地测试我的 VolunteerRepository.GetAllVolunteers() 方法是否有效?
编辑(解决方案):
var mockedDbCtxVolunteer = new Mock<DbSet<Volunteer>>();
mockedDbCtxVolunteer.As<IQueryable<Volunteer>>().Setup(m => m.GetEnumerator()).Returns(expected.GetEnumerator());
我们正在使用助手 class 来模拟 DbSet。它为集合中的模拟项目使用内部数据存储。
public class MockDbSet<T> : IDbSet<T> where T : class {
readonly HashSet<T> _data;
readonly IQueryable _query;
public MockDbSet() : this(new HashSet<T>()) {
}
public MockDbSet(params T[] entries) : this(new HashSet<T>(entries)) {
}
private MockDbSet(IEnumerable<T> data) {
_data = new HashSet<T>(data);
_query = _data.AsQueryable();
}
public T Add(T item) {
_data.Add(item);
return item;
}
public T Remove(T item) {
_data.Remove(item);
return item;
}
Type IQueryable.ElementType {
get { return _query.ElementType; }
}
IEnumerator IEnumerable.GetEnumerator() {
return _data.GetEnumerator();
}
// implement other members of IDbSet<T> ...
}
然后在您的单元测试中,使用 Moq 设置数据上下文。对 Volunteer
集的访问将被重定向到模拟实现。
var mockedVolunteer1 = new Volunteer { Name = "Uno" };
var mockedDbSet = new MockDbSet<Volunteer> {
mockedVolunteer1, mockedVolunteer2, mockedVolunteer3
};
var mockedContext = new Mock<MyDataContext>();
mockedContext.Setup(ctx => ctx.Volunteer).Returns(mockedDbSet);
使用 MockDbSet class 的优点是您不必猜测您的测试将访问哪些方法因此需要模拟,并且您不必每次都设置模拟方法.
ToList()
is an extension method, which Moq (and other libraries) cannot mock。不要担心模拟那个方法,而是模拟它会使用什么:GetEnumerator()
:
var mockedCtxVolunteer = new Mock<DbSet<Volunteer>>();
mockedCtxVolunteer.Setup(m => m.GetEnumerator()).Returns(expected.GetEnumerator());
我是单元测试的新手,希望编写一个 N 层的应用程序,因此我的 ORM 是从 BLL 中抽象出来的,中间有一个存储库模式。我将如何对与 EF (DbContext) 耦合的存储库模式方法进行单元测试?
我的SUT方法在这里:
public IList<Volunteer> GetAllVolunteers() {
return dbctx.Volunteer.ToList();
}
到目前为止我的单元测试:
[Fact]
public void GetAllVolunteersMethodWorks() {
// Arrange
var fakeVolunteer = new Volunteer { Id = 0, Name = "Dummy" };
var expected = new List<Volunteer>(new Volunteer[] { fakeVolunteer });
var mockedCtxVolunteer = new Mock<DbSet<Volunteer>>();
mockedCtxVolunteer.Setup(m => m.ToList()).Returns(expected);
var mockedctx = new Mock<RSMContext>();
mockedctx.Setup(m => m.Volunteer).Returns(mockedCtxVolunteer.Object);
// Act
var volunteerRepo = new VolunteerRepository(mockedctx.Object);
var result = volunteerRepo.GetAllVolunteers();
// Assert
Assert.Equal(expected, result);
}
我得到一个错误 System.NotSupportedException:'表达式引用了一个不属于模拟对象的方法:m => m.ToList() 那么我该如何正确地模拟 DbSet,以便我的假数据驻留在那里以正确地测试我的 VolunteerRepository.GetAllVolunteers() 方法是否有效?
编辑(解决方案):
var mockedDbCtxVolunteer = new Mock<DbSet<Volunteer>>();
mockedDbCtxVolunteer.As<IQueryable<Volunteer>>().Setup(m => m.GetEnumerator()).Returns(expected.GetEnumerator());
我们正在使用助手 class 来模拟 DbSet。它为集合中的模拟项目使用内部数据存储。
public class MockDbSet<T> : IDbSet<T> where T : class {
readonly HashSet<T> _data;
readonly IQueryable _query;
public MockDbSet() : this(new HashSet<T>()) {
}
public MockDbSet(params T[] entries) : this(new HashSet<T>(entries)) {
}
private MockDbSet(IEnumerable<T> data) {
_data = new HashSet<T>(data);
_query = _data.AsQueryable();
}
public T Add(T item) {
_data.Add(item);
return item;
}
public T Remove(T item) {
_data.Remove(item);
return item;
}
Type IQueryable.ElementType {
get { return _query.ElementType; }
}
IEnumerator IEnumerable.GetEnumerator() {
return _data.GetEnumerator();
}
// implement other members of IDbSet<T> ...
}
然后在您的单元测试中,使用 Moq 设置数据上下文。对 Volunteer
集的访问将被重定向到模拟实现。
var mockedVolunteer1 = new Volunteer { Name = "Uno" };
var mockedDbSet = new MockDbSet<Volunteer> {
mockedVolunteer1, mockedVolunteer2, mockedVolunteer3
};
var mockedContext = new Mock<MyDataContext>();
mockedContext.Setup(ctx => ctx.Volunteer).Returns(mockedDbSet);
使用 MockDbSet class 的优点是您不必猜测您的测试将访问哪些方法因此需要模拟,并且您不必每次都设置模拟方法.
ToList()
is an extension method, which Moq (and other libraries) cannot mock。不要担心模拟那个方法,而是模拟它会使用什么:GetEnumerator()
:
var mockedCtxVolunteer = new Mock<DbSet<Volunteer>>();
mockedCtxVolunteer.Setup(m => m.GetEnumerator()).Returns(expected.GetEnumerator());