我们刚刚为我们的专有系统发布了一个重写(第三次)模块。这个模块,我们称之为负载管理器,是迄今为止我们系统中所有模块中最复杂的模块。我们正在努力获得一个全面的测试套件,因为每次我们对这个模块进行任何重大改变时,都需要花费数周的时间来解决错误和怪癖。但是,开发测试套件已经证明非常困难,因此我们正在寻找创意。
Load Manager的内容驻留在一个名为LoadManagerHandler的类中,这基本上是模块背后的所有逻辑。此处理程序调用多个控制器来执行数据库中的CRUD方法。这些控制器本质上是位于顶部的DAL的顶层,并抽象出我们的LLBLGen生成的代码。
因此,使用Moq框架模拟这些控制器是很容易的。然而,问题出在Load Manager的复杂性上,我们收到的问题不是处理简单的情况,而是处理程序中包含大量数据的情况。
简要说明加载管理器包含许多“已卸载”的详细信息,有时会有数百个,然后放入用户创建的加载和重装池中。在创建和填充这些加载的过程中,会出现大量删除,更改和添加,最终会导致出现问题。但是,因为当你模拟一个对象的方法时,最后一个模拟获胜,即:
jobDetailControllerMock.Setup(mock => mock.GetById(1)).Returns(jobDetail1);
jobDetailControllerMock.Setup(mock => mock.GetById(2)).Returns(jobDetail2);
jobDetailControllerMock.Setup(mock => mock.GetById(3)).Returns(jobDetail3);
无论我发送到jobDetailController.GetById(x)的什么,我总会回来jobDetail3。这使得测试几乎不可能,因为我们必须确保在进行更改时所有受影响的点都会受到影响。
所以,我决定使用测试数据库,只是允许读取和写入正常进行。但是,因为您不能(读取:不应该)指示测试的顺序,所以先前运行的测试可能导致稍后运行的测试失败。
TL / DR:我基本上在寻找面向数据的代码的测试策略,这些代码本质上非常复杂。答案 0 :(得分:2)
如Seb所述,您确实可以使用范围匹配:
controller.Setup(x => x.GetById(It.IsInRange<int>(1, 3, Range.Inclusive))))).Returns<int>(i => jobs[i]);
此代码使用传递给方法的参数来计算要返回的值。
答案 1 :(得分:1)
要了解Moq的“最后模拟胜利”,您可以使用此博客中的技术:
Moq Triqs - Successive Expectations
修改强>
实际上你根本不需要那个。根据您的示例,Moq将根据方法参数返回不同的值。
public interface IController
{
string GetById(int id);
}
class Program
{
static void Main(string[] args)
{
var mockController = new Mock<IController>();
mockController.Setup(x => x.GetById(1)).Returns("one");
mockController.Setup(x => x.GetById(2)).Returns("two");
mockController.Setup(x => x.GetById(3)).Returns("three");
IController controller = mockController.Object;
Console.WriteLine(controller.GetById(1));
Console.WriteLine(controller.GetById(3));
Console.WriteLine(controller.GetById(2));
Console.WriteLine(controller.GetById(3));
Console.WriteLine(controller.GetById(99) == null);
}
}
class Program
{
static void Main(string[] args)
{
var mockController = new Mock<IController>();
mockController.Setup(x => x.GetById(1)).Returns("one");
mockController.Setup(x => x.GetById(2)).Returns("two");
mockController.Setup(x => x.GetById(3)).Returns("three");
IController controller = mockController.Object;
Console.WriteLine(controller.GetById(1));
Console.WriteLine(controller.GetById(3));
Console.WriteLine(controller.GetById(2));
Console.WriteLine(controller.GetById(3));
Console.WriteLine(controller.GetById(99) == null);
}
}
输出是:
one three two three True
答案 2 :(得分:1)
听起来像LoaderManagerHandler确实......相当多的工作。班级名称中的“经理”总是让我担心......从TDD的角度来看,如果可能的话,可能值得考虑适当地打破课程。
这堂课多长时间了?
答案 3 :(得分:0)
我从未使用过Moq,但它似乎应该能够通过提供的参数匹配模拟调用。
快速浏览一下Quick Start documentation,摘录如下:
//Matching Arguments
// any value
mock.Setup(foo => foo.Execute(It.IsAny<string>())).Returns(true);
// matching Func<int>, lazy evaluated
mock.Setup(foo => foo.Add(It.Is<int>(i => i % 2 == 0))).Returns(true);
// matching ranges
mock.Setup(foo => foo.Add(It.IsInRange<int>(0, 10, Range.Inclusive))).Returns(true);
我认为你应该能够使用上面的第二个例子。
答案 4 :(得分:0)
一种简单的测试技术是确保每次针对系统记录错误时,请确保编写覆盖该案例的单元测试。您可以使用该技术构建一组非常可靠的测试。更好的是,你不会两次遇到同样的事情。
答案 5 :(得分:0)
无论我发送到jobDetailController.GetById(x),我总会找回jobDetail3
您应该花更多时间调试测试,因为发生的事情并不是Moq的行为方式。您的代码或测试中存在一个错误,导致行为异常。
如果您想使用相同的输入但不同的输出重复调用,您还可以使用不同的模拟框架。 RhinoMocks支持录制/播放习惯用法。你是对的,这并不总是你想要的强制执行电话订单。我更喜欢Moq,因为它简单。