FakeItEasy说MustHaveHappened没有发生...但实际上是发生了。

7

我正在尝试对“服务层” / “应用程序门面层”方法进行单元测试。 这是我要进行单元测试的方法:

// Create a new order in the database for a customer.  Given a customer id,
// will create a new order and return an OrderDto for use in the presentation
// layer.
public OrderDto CreateOrderForCustomer(int customerId)
{
  // Find the customer
  var customer = _customerRepository.GetCustomerById(customerId);

  // Create an order and apply special logic to get it ready for use.
  var orderFactory = new OrderFactory();
  var order = orderFactory.CreateOrder(customer);

  // IMPORTANT: This is what I'm trying to unit test ...
  _orderRepository.Save(order);

  order.Status = "Editing";

  // Using AutoMapper to turn this into a DTO that will be returned
  // to the Presentation layer.  The Mappings are created in the 
  // constructor and not depicted in this code snippet.
  var orderDto = Mapper.Map<Order, OrderDto>(order);

  return orderDto;
}

(Note ... 为了更清晰地表述,我在这里添加了大量注释。我通常不会这么啰嗦。)
由于这种方法的工作是将领域层方法和持久层方法编排成创建一个空订单、将其持久化并将其作为简单的 DTO 返回,所以我觉得这是 FakeItEasy 的一项重要工作...... 我只需确保这些关键方法被正确编排,确保它们被调用使用 FakeItEasy 的 MustHaveHappened()。
因此,考虑到这一点,这就是我创建的单元测试:
[TestMethod]
public void CreateOrderForCustomer_ValidCustomer_CreatesNewOrder()
{
  // Arrange
  var customer = _customerRepository.GetCustomerById(1);
  Assert.AreEqual(0, customer.Orders.Count);

  // Act
  var orderDto = _orderEntryService.CreateOrderForCustomer(1);

  // Assert

  // Here I'm trying to make sure to re-create the order that was actually 
  // sent into the _customerRepository.Save() ... I should be able to
  // simple un-map the OrderDto back to an Order, and undo the property 
  // change.
  var order = Mapper.Map<OrderDto, Order>(orderDto);
  order.Status = "New";

  A.CallTo(() => _customerRepository.GetCustomerById(1)).MustHaveHappened();

  // **THIS CAUSES AN EXCEPTION**
  A.CallTo(() => _orderRepository.Save(order)).MustHaveHappened();
  Assert.AreEqual(1, customer.Orders.Count);
}

在单元测试中,我无法访问在测试方法中创建的实际订单(ACTUAL Order),我试图做到下一个最好的事情...取决于通过测试方法返回的DTO版本的订单,将该订单的DTO版本映射回一个新的领域模型Order实例,并确保属性相同,然后将其发送到FakeItEasy的MustHaveHappened()函数。
我调试了单元测试并查看了实际订单的属性与虚假订单的属性......我向您保证,它们是相同的。此外,通过调试,我可以确认_customerRepository.Save(order)确实被调用了。
问题:MustHaveHappened()失败是否因为我基本上发送了两个不同的订单对象实例 - 即使它们的属性是相同的?即使属性相同,FakeItEasy是否需要相同的输入参数实例来确保方法调用已发生?
此外,有什么建议可以测试这种类型的东西(即编排/服务/“应用程序外观”/任何你想称呼的层方法)吗?
1个回答

18

.MustHaveHappened()失败是因为我基本上发送了两个不同的Order对象实例吗——即使它们的属性相同吗?

是的。FakeItEasy将使用.Equals,它(除非您的类覆盖它)对于引用类型默认为引用相等性。

(...) FakeItEasy需要相同的输入参数实例来确保方法调用已发生吗?

不需要。您可以像这样进行自定义参数匹配:

A.CallTo(() => _orderRepository.Save(A<Order>.That.Matches(o =>
    o.Status == "New" &&
    o.Id == 10
))).MustHaveHappened();

然而,这个问题揭示了你的代码存在的一个问题。从你的示例中可以清楚地看到,你正在将_customerRepository注入为一个依赖项。这很好。为什么不将OrderFactory也同样进行依赖项注入呢?如果通过接口/基类依赖项进行注入,则可以轻松地模拟(虚假)它,并且您当前的问题就不会存在。

如果您可以更改代码,我建议注入工厂(遵循简单的指导原则 - "没有new是好消息!")。如果不能更改代码,请使用自定义匹配器来验证订单属性,就像我在上面的示例中所做的那样。


1
并不是使用引用比较,而是使用Equals实现。因此,如果您重写Equals方法以将等效订单视为相等,则也可以解决问题。 - Patrik Hägne
@PatrikHägne:是的,我的心理快捷方式有些失控了,感谢提醒。 - k.m

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接