如何用 Rhino 模拟存根 SingleOrDefault
本文关键字:存根 SingleOrDefault 模拟 Rhino 何用 | 更新日期: 2023-09-27 18:33:45
我很难理解我在这里是否有正确的方法。我想测试存储库。存储库依赖于 DbContext。我希望能够验证存储库没有在 IDbSet 类型的属性上调用函数 Add,该属性是 DbContext 的成员。
我尝试了两种方法。用行为验证,用状态验证。似乎用行为验证是正确的,因为谁知道存根状态在假对象中做什么。
public void VerifyBehaviour()
{
// Arrange
var stubEntities = MockRepository.GenerateStub<IWsStatContext>();
var stubManufcturers = MockRepository.GenerateStub<IDbSet<Manufacturer>>();
var manufacturer = new Manufacturer() { Name = "Dummy" };
var manufacturers = new List<Manufacturer>();
manufacturers.Add(manufacturer);
stubManufcturers.Stub(x => x.Local).Return(new System.Collections.ObjectModel.ObservableCollection<Manufacturer>(manufacturers));
stubManufcturers.Stub(x => x.SingleOrDefault(m => m.Name == "Dummy")).Return(manufacturer);
stubEntities.Manufacturers = stubManufcturers;
// Act
var sut = new EquiptmentRepository(stubEntities);
sut.AddManufacturer(manufacturer);
// Assert
stubManufcturers.AssertWasNotCalled(x => x.Add(manufacturer));
}
public void VerifyState()
{
// Arrange
var stubEntities = MockRepository.GenerateStub<IWsStatContext>();
var stubManufacturers = new InMemoryDbSet<Manufacturer>();
var manufacturer = new Manufacturer() { Name = "Dummy" };
stubManufacturers.Add(manufacturer);
stubEntities.Manufacturers = stubManufacturers;
// Act
var sut = new EquiptmentRepository(stubEntities);
sut.AddManufacturer(manufacturer);
// Assert
Assert.AreEqual(stubManufacturers.Count(), 1);
}
验证行为方法是否失败,并在 SingleOrDefault 的存根周围出现 NullReferenceException。所以我发现帖子说最好验证状态并使用假的 DbSet。但是检查假物体的状态感觉不对。如果 Add 函数的实现方式与实际函数不同怎么办(它最初是这样,即使我的存储库已损坏,我的测试也通过了(。
有谁知道如何存根 SingleOrDefault,以便我可以检查调用了添加?我无法检查 Add 是在非犀牛嘲笑的存根上调用的。
谢谢
正如jimmy_keen的回答所述:
SingleOrDefault
是在IEnumerable<T>
上定义的扩展方法(IDbSet<T>
实现(。扩展方法意味着它是一种静态方法。RhinoMocks(或任何其他免费工具(不能模拟/存根静态方法。
与其尝试"存根"扩展方法,不如尝试存根构建扩展方法的底层接口:IEnumerable<T>
stubManufcturers.Stub( x => x.GetEnumerator() ).Return( new List<Manufacturer> { manufacturer }.GetEnumerator() );
通过在调用 SingleOrDefault
时存根 GetEnumerator()
的行为,它将按预期针对假枚举执行,并且测试将能够评估该行为。
SingleOrDefault
是在IEnumerable<T>
上定义的扩展方法(IDbSet<T>
实现(。扩展方法意味着它是一种静态方法。RhinoMocks(或任何其他免费工具(不能模拟/存根静态方法。
在这里没有太多选择:您要么必须进行基于状态的验证,要么创建手工制作的模拟并为您的测试手动设置(但这很可能最终会再次进行基于状态的验证 - 你不能真正存根SingleOrDefault
(。
编辑:提取和覆盖示例:
首先,您需要提取类中存在问题的部分以分离稍后将被覆盖的方法。这个有问题的部分自然是与IDbSet
的相互作用:
public class EquiptmentRepository
{
public void Add(Manufacturer m)
{
// perform some local logic before calling IDbSet.Add
this.AddToDbSet(m);
}
protected virtual AddToDbSet(Manufacturer m)
{
this.context.Manfuacturers.Add(m);
}
}
现在,在可测试的EquiptmentRepository
版本中,您可以覆盖AddToDbSet
以使测试更容易,例如,只需添加一些状态验证:
internal void TestableEquiptmentRepository: EquiptmentRepository
{
internal List<Manufacturer> AddedManufacturers = new List<Manufacturer>();
protected override void AddToDbSet(Manufacturer m)
{
// we're no longer calling DbSet.Add but kind of rolling
// our own basic mock and tracking what objects were
// add by simply adding them to internal list
this.AddedManufacturers.Add(m);
}
}
每当调用Add
时,如果在实际情况下将其添加到DbSet
,则传递的制造商将被添加到列表中。在测试中,您只需验证类状态的可测试版本:
[Test]
public void AddManufacturer_DoesNotAddExistingManufacturersToDbSet()
{
// Arrange
var stubEntities = MockRepository.GenerateStub<IWsStatContext>();
var stubManufacturers = MockRepository.GenerateStub<IDbSet<Manufacturer>>();
var manufacturer = new Manufacturer() { Name = "Dummy" };
stubManufacturers.Add(manufacturer);
stubEntities.Manufacturers = stubManufacturers;
// Act
var sut = new TestableEquiptmentRepository(stubEntities);
sut.AddManufacturer(manufacturer);
// Assert
Assert.AreEqual(sut.AddedManufacturers.Count(), 0);
}
通过这种方式,您可以验证EquiptmentRepository.Add
中的所有逻辑,而无需与DbSet
进行交互。
你不能用 RhinoMocks 或 Moq 来模拟静态方法(TypeMock 可以(。
对假对象的状态验证实际上验证了您的模拟,而不是您正在测试的系统。
有一种技术可以使您的代码可测试(但我认为价格太高(。您必须提取扩展方法进行接口,并将 System.Linq.Enumerable 扩展的使用替换为您自己的扩展:
var item = items.MyExtensions().SingleOrDefault();
顺便说一句,当我面对静态方法模拟时,我通常会做以下事情之一:
- 静态方法执行的传递结果。 例如,如果我在测试方法中需要当前日期,而不是调用
DateTime.Today
我将当前时间作为参数传递Foo(DateTime date)
。 - 例如,如果我需要获取一些配置设置,而不是调用
ConfigurationManager.AppSettings["foo"]
,我创建非静态类BarConfiguration
将所有工作委托给静态 ConfigurationManager(并实现接口IBar { int Foo { get; }
(。 - 不要嘲笑它。看起来我正在测试一些我不应该测试的东西(可枚举的扩展、日志等(
请考虑不要对存储库进行单元测试。数据访问逻辑的集成测试更有意义。