moq.Callback(),未知






4.96/5 (9投票s)
在单元测试中何时以及如何使用Moq上的Callback方法。
引言
我越来越频繁地看到人们在测试某些类型的代码时遇到麻烦。结果,代码覆盖率下降,未经验证的逻辑显现,质量下降,挫败感上升。这就是促使我写这篇帖子来描述这种情况以及一种体面的解决方案。
这种情况常见于被测单元修改传递给模拟对象的参数的所有情况。在这种情况下,我们需要验证这种转换是否达到了我们预期的效果。然而,这并不像看起来那么简单明了。因为代码胜过千言万语,让我们通过一个例子来说明。
考虑以下类public class ProductService
{
private readonly IOrderRepository m_orderRepository;
public ProductService(IOrderRepository orderRepository)
{
m_orderRepository = orderRepository;
}
public List GetProducts(int customerId, int orderId)
{
OrderSearchCriteria orderSearchCriteria = new OrderSearchCriteria
{
OrderId = customerId // THIS IS THE PROBLEM WE ARE GOING TO SEARCH FOR
// Set some other search criteria...
};
Order retrievedOrder = m_orderRepository.GetOrder(orderSearchCriteria);
// Do something else
return retrievedOrder.Products;
}
}
您在这里看到的是一个我们将要测试的假设性的产品服务。更确切地说,我们将为 GetProducts
方法编写单元测试。该方法特别做的事情是组合另一个对象,该对象将被传递给我们的依赖项,即订单存储库。现在,您可以争辩说这是一种不好的做法,对象组合应该以不同的方式处理,因为通常在这种情况下,单一职责原则没有得到满足。您说得对,但我们并不生活在一个完美的世界里,而且我们经常无法轻易地改变现有的东西。然而,我们需要不断扩展和改进我们的软件。
不过,我仍然需要为该方法编写一个测试。我该怎么办,我该如何发现我们刚刚引入的这个错误?
有两种方法可以编写一个单元测试,该测试将测试、验证并发现我们的错误。让我们从第一种方法开始。
塑造预期的实例
我们可以通过在我们的测试中手动设置一个 OrderSearchCriteria
类的实例来解决这个问题,正如我们所期望的那样,基于我们传入的参数,并确保我们的模拟对象只接受一个与我们创建的特意创建的类相等的实例。
让我们检查一下我们的单元测试。
[TestMethod]
public void GetProducts_Creates_OrderSearchCriteria_Correctly()
{
const int customerId = 56789;
const int orderId = 12345;
OrderSearchCriteria orderSearchCriteria = new OrderSearchCriteria
{
OrderId = orderId
};
Mock orderRepositoryMock = new Mock();
orderRepositoryMock
.Setup(m => m.GetOrder(orderSearchCriteria))
.Returns(new Order());
ProductService sut = new ProductService(orderRepositoryMock.Object);
List result = sut.GetProducts(customerId, orderId);
}
首先,要使此示例正常工作,您的参数类需要实现相等成员。这是必需的,因为 Moq 为了确定参数的相等性,正确地依赖于 Equals()
方法。
这种技术的另一个缺点是,有时构建我们自己的对象可能很困难,甚至不可能。更不用说我们将引入的维护问题了。
由于 Moq 在参数错误的情况下会从方法调用返回 null,因此通常会处理 null 值并将其解释为一种可能的状态。在这种情况下,发现我们的错误将非常困难或不可能。
幸运的是,有一种更简洁的方法来处理这种情况。
通过 Callback 方法提取参数
由于不经常使用,许多开发人员倾向于忽略 Moq 框架提供的 Callback()
方法。在这种情况下,它可能非常方便。
看看下面的测试。
[TestMethod]
public void GetProducts_Creates_OrderSearchCriteria_Correctly_2()
{
const int customerId = 56789;
const int orderId = 12345;
OrderSearchCriteria recievedOrderSearchCriteria = null;
Mock orderRepositoryMock = new Mock();
orderRepositoryMock
.Setup(m => m.GetOrder(It.IsAny<OrderSearchCriteria>()))
.Returns(new Order())
.Callback(o => recievedOrderSearchCriteria = o);
ProductService sut = new ProductService(orderRepositoryMock.Object);
List result = sut.GetProducts(customerId, orderId);
Assert.IsNotNull(recievedOrderSearchCriteria);
Assert.AreEqual(orderId, recievedOrderSearchCriteria.OrderId);
}
您可以看到我正在设置我的模拟对象,并指定回调应该做什么。在这种情况下,我告诉他,对于类型为 OrderSearchCriteria
的参数,一旦调用该方法,就将其复制到本地定义的名为 recievedOrderSearchCriteria
的对象中。这将使我有可能检查 GetOrder
方法调用中传入的内容,并验证它是否符合我的预期。
一旦我开始断言,我就会对 recievedOrderSearchCriteria
进行检查,并确保传入的内容符合我的预期。
此测试将失败,我们将成功实现我们的目标。而且,我们收到的消息比前一个示例中的要清楚得多。此时它显示
Assert.AreEqual 失败。预期:<12345>。实际:<56789>。除此之外,我们实际上在断言预期的结果,从而以明确的方式指定行为。现在,在我看来,这要好得多!
关于 Callback 方法的其他考虑
对于经验较少的开发人员,我还会举一个如何使回调正常工作的例子,如果您有多个参数被我们的模拟对象接受。
我将通过添加一个接受两个参数的 GetOrder
方法的重载来扩展我的 IOrderRepository
接口。我还将在 ProductService
类中实现另一个使用这个新创建的方法。
public interface IOrderRepository
{
Order GetOrder(OrderSearchCriteria searchCriteria);
Order GetOrder(int orderId, bool archieved);
}
public List GetProducts(int orderId)
{
Order retrievedOrder = m_orderRepository.GetOrder(orderId, true);
return retrievedOrder.Products;
}
为了模拟我的订单存储库并在回调中获得必要的值,使用了以下测试。[TestMethod]
public void GetProducts_With_Archieved_Orders()
{
const int orderId = 12345;
int receivedOrderId = 0;
bool receivedArchieved = false;
Mock orderRepositoryMock = new Mock();
orderRepositoryMock
.Setup(m => m.GetOrder(It.IsAny(), It.IsAny()))
.Returns(new Order())
.Callback((o, a) =>
{
receivedOrderId = o;
receivedArchieved = a;
});
ProductService sut = new ProductService(orderRepositoryMock.Object);
List result = sut.GetProducts(orderId);
Assert.AreEqual(orderId, receivedOrderId);
Assert.AreEqual(true, receivedArchieved);
}
如您所见,我只是在我的泛型定义中添加了一个额外的类型,然后相应地调整了我的 lambda 表达式。如果需要两个以上的参数,您可以遵循相同的模式并定义任意多个。例如 .Callback((o, a, i)
等。
您始终需要遵守您正在设置的方法的确切参数签名。被模拟方法接受的参数数量需要在类型、顺序和数量上与您的被模拟方法接受的参数相匹配。
还有另一种表达相同语句的方法,只需使用 lambda 表达式而不是泛型。我可以将上面的例子重写如下
.Callback((OrderSearchCriteria o) => recievedOrderSearchCriteria = o);
// ...
.Callback((int o, bool a) =>
{
receivedOrderId = o;
receivedArchieved = a;
});
效果是相同的,这仅仅取决于您的偏好,您选择使用哪种方式。也可以在方法调用之前和之后定义它,而且由于我想不到一个很好的例子,我将只提供 Moq 文档中的示例。
// callbacks can be specified before and after invocation
mock.Setup(foo => foo.Execute("ping"))
.Callback(() => Console.WriteLine("Before returns"))
.Returns(true)
.Callback(() => Console.WriteLine("After returns"));
结论
每次您需要检查传递给您正在设置的方法的参数时,Callback()
都会帮助您获取它们。此外,如果您需要在调用方法之前或之后执行任何代码,Callback()
会让您做到这一点。希望您不会每天都使用它,但知道它很有用,因为迟早您会遇到 Callback()
能够帮助您实现目标的情况。