使用 Moq 测试递归函数
Test a recursive Function with Moq
我有一个递归方法,为了便于阅读,我试图简化它,因为它是一个混乱的方法。
我想获得 100% 的递归方法覆盖率。我正在努力设置将经过两次递归的模拟。然后退出。我想我需要使用回调而不是 returns,我不是 100% 确定。
[TestMethod]
public void TestRecursiveLoop()
{
var rootList = new List<MyObject> { new MyObject { Id = 999, ParentId = 1000 }, new MyObject { Id = 1000 } };
var myObject = new MyObject { Id = 999 };
var myMock = new Mock<IDBCLASS>();
myMock.Setup(o => o.GETBYType(It.IsAny<Expression<Func<MyObject, bool>>>()))
.Returns((Expression<Func<ShareholderTransaction, bool>> predicate) =>
{
return new List<MyObject>().Where(o => o.ParentId == myObject.Id).ToList();
});
// ?????????? not sure if this is correct
var testRecursion = new TestRecursion(myMock.Object);
testRecursion.Recursive(999, rootList);
}
}
public class TestRecursion
{
private IDBCLASS dbClass;
public TestRecursion(IDBCLASS dbClass)
{
this.dbClass = dbClass;
}
public void Recursive(int id, List<MyObject> list)
{
var parentObject = this.dbClass.GETBYType<MyObject>(x => x.ParentId == id).FirstOrDefault();
if (parentObject == null)
{
return;
}
list.Add(parentObject);
Recursive(parentObject.Id, list);
}
}
public class MyObject
{
public int ParentId { get; set; }
public int Id { get; set; }
}
public class DBCLASS : IDBCLASS
{
public List<T> GETBYType<T>(Expression<Func<T, bool>> p) where T : class
{
// return something from DB;
}
}
public interface IDBCLASS
{
List<T> GETBYType<T>(Expression<Func<T, bool>> p) where T : class;
}
你基本上有两个不同的选择:
要么你有一个很大的 Arrange 部分(如在 AAA 中,Arrange Act Assert),要么你使用了一个小缺点:
大排列方法
正如@Brian 在对您的问题的评论中所说,您可以在每次调用时将 GETBYType
设置为 return 不同的值。
他提出的方法可行,但是,存在一种简单、更清晰的方法:您可以使用 SetupSequence 方法。
本质上,用法应该是这样的:
var myMock = new Mock<IDBCLASS>();
myMock.SetupSequence(o => o.GETBYType(It.IsAny<Expression<Func<MyObject, bool>>>()))
.Returns(new MyObject { Id = 999, ParentId = 1000 })
.Returns(new MyObject { Id = 1000 } )
.Returns(null);
自我黑客
还有另一种方法,不需要您对整个递归进行复杂的排列。它背后的想法是只断言 递归 继续正确。
我们在这里面临的问题是,由于是递归调用,我们无法对被测对象进行断言。为了解决这个问题,我们可以利用一些技巧。它不是很干净,但它可以让你大大减少测试的数量。
诀窍是在您的 TestRecursion
class 中注入类型 TestRecursion
的对象。那时,您将有两个构造函数:一个无参数构造函数和一个接受 TestRecursion
实例的构造函数。接受 TestRecursion
对象的构造函数将把它作为一个字段(我们称它为 this.self),以便调用该对象的递归方法。
现在,接受 TestRecursion
对象的构造函数应该 永远不会 被生产代码使用 - 只能被测试使用,except 在一个地方:无参数构造函数将调用构造函数期望 TestRecursion
对象,将 this
作为参数传递。
除此异常外,生产代码将始终调用无参数构造函数。不幸的是,这个无参数构造函数不容易测试,这是变通方法的一大缺点。但是,现在可以很容易地从测试中注入模拟的 TestRecursion
并验证递归方法是否使用正确的参数调用了正确的次数。
当然,Recursive
方法的实现应该稍微修改一下,替换下面一行
Recursive(parentObject.Id, list);
由
this.self.Recursive(parentObject.Id, list);
到那时,测试停止条件也应该容易得多。
希望这对您有所帮助。
我有一个递归方法,为了便于阅读,我试图简化它,因为它是一个混乱的方法。
我想获得 100% 的递归方法覆盖率。我正在努力设置将经过两次递归的模拟。然后退出。我想我需要使用回调而不是 returns,我不是 100% 确定。
[TestMethod]
public void TestRecursiveLoop()
{
var rootList = new List<MyObject> { new MyObject { Id = 999, ParentId = 1000 }, new MyObject { Id = 1000 } };
var myObject = new MyObject { Id = 999 };
var myMock = new Mock<IDBCLASS>();
myMock.Setup(o => o.GETBYType(It.IsAny<Expression<Func<MyObject, bool>>>()))
.Returns((Expression<Func<ShareholderTransaction, bool>> predicate) =>
{
return new List<MyObject>().Where(o => o.ParentId == myObject.Id).ToList();
});
// ?????????? not sure if this is correct
var testRecursion = new TestRecursion(myMock.Object);
testRecursion.Recursive(999, rootList);
}
}
public class TestRecursion
{
private IDBCLASS dbClass;
public TestRecursion(IDBCLASS dbClass)
{
this.dbClass = dbClass;
}
public void Recursive(int id, List<MyObject> list)
{
var parentObject = this.dbClass.GETBYType<MyObject>(x => x.ParentId == id).FirstOrDefault();
if (parentObject == null)
{
return;
}
list.Add(parentObject);
Recursive(parentObject.Id, list);
}
}
public class MyObject
{
public int ParentId { get; set; }
public int Id { get; set; }
}
public class DBCLASS : IDBCLASS
{
public List<T> GETBYType<T>(Expression<Func<T, bool>> p) where T : class
{
// return something from DB;
}
}
public interface IDBCLASS
{
List<T> GETBYType<T>(Expression<Func<T, bool>> p) where T : class;
}
你基本上有两个不同的选择:
要么你有一个很大的 Arrange 部分(如在 AAA 中,Arrange Act Assert),要么你使用了一个小缺点:
大排列方法
正如@Brian 在对您的问题的评论中所说,您可以在每次调用时将 GETBYType
设置为 return 不同的值。
他提出的方法可行,但是,存在一种简单、更清晰的方法:您可以使用 SetupSequence 方法。
本质上,用法应该是这样的:
var myMock = new Mock<IDBCLASS>();
myMock.SetupSequence(o => o.GETBYType(It.IsAny<Expression<Func<MyObject, bool>>>()))
.Returns(new MyObject { Id = 999, ParentId = 1000 })
.Returns(new MyObject { Id = 1000 } )
.Returns(null);
自我黑客
还有另一种方法,不需要您对整个递归进行复杂的排列。它背后的想法是只断言 递归 继续正确。
我们在这里面临的问题是,由于是递归调用,我们无法对被测对象进行断言。为了解决这个问题,我们可以利用一些技巧。它不是很干净,但它可以让你大大减少测试的数量。
诀窍是在您的 TestRecursion
class 中注入类型 TestRecursion
的对象。那时,您将有两个构造函数:一个无参数构造函数和一个接受 TestRecursion
实例的构造函数。接受 TestRecursion
对象的构造函数将把它作为一个字段(我们称它为 this.self),以便调用该对象的递归方法。
现在,接受 TestRecursion
对象的构造函数应该 永远不会 被生产代码使用 - 只能被测试使用,except 在一个地方:无参数构造函数将调用构造函数期望 TestRecursion
对象,将 this
作为参数传递。
除此异常外,生产代码将始终调用无参数构造函数。不幸的是,这个无参数构造函数不容易测试,这是变通方法的一大缺点。但是,现在可以很容易地从测试中注入模拟的 TestRecursion
并验证递归方法是否使用正确的参数调用了正确的次数。
当然,Recursive
方法的实现应该稍微修改一下,替换下面一行
Recursive(parentObject.Id, list);
由
this.self.Recursive(parentObject.Id, list);
到那时,测试停止条件也应该容易得多。
希望这对您有所帮助。