FakeItEasy says MustHaveHappened didn't happen ... but it did
Asked Answered
Q

1

7

I'm trying to unit test a "service layer" / "application facade layer" method. This is the method I'm trying to unit test:

// 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 ... I've added copious notes here for clarity. I'm not usually this chatty.)

Since this method's job is to orchestrate Domain Layer methods and Persistence Layer methods into creating an empty order, persist it, and return it as a simple DTO, I figured this was a great job for FakeItEasy ... I'll just make sure those critical methods are being orchestrated properly making sure they get called using FakeItEasy's MustHaveHappened().

So, with that in mind, here's the unit test I created:

[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);
}

In the unit test, I can't access the ACTUAL Order that was created in the method under test, I try to do the next best thing ... take the DTO version of the Order that is RETURNED by the method under test, map the DTO version of the Order back out to a new instance of the domain model Order and make sure the properties are the same before sending it to FakeItEasy's MustHaveHappened().

I've debugged the unit test and eyed up the ACTUAL Order's properties versus the FAKED Order's properties ... I assure you, they are identical. Also, I can confirm, through debugging, that the _customerRepository.Save(order) is indeed being called.

QUESTION Is .MustHaveHappened() failing because I'm essentially sending in two different instances of the Order object -- even though their properties are identical? Even though the properties are the same, does FakeItEasy need the same instance of the input parameter to ensure that the method call has happened?

Furthermore, any suggestions for how I should be testing this sort of thing (i.e., an orchestration / service / "application facade" / what-ever-you-want-to-call-it layer method)?

Quartern answered 18/12, 2012 at 16:23 Comment(0)
N
18

Is .MustHaveHappened() failing because I'm essentially sending in two different instances of the Order object -- even though their properties are identical?

Yes. FakeItEasy will use .Equals, which (unless your class overrides it) for reference types defaults to reference equality.

(...) does FakeItEasy need the same instance of the input parameter to ensure that the method call has happened?

No. You can do custom argument matching like this:

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

But, this problem revealed an issue with your code. From your sample it's clear you're injecting _customerRepository as a dependency. That's great. Why don't you do the same with OrderFactory? If it were injected via interface/base class dependency, you could then easily mock (fake) it and your current problem wouldn't exist.

If you can alter your code I'd suggest injecting the factory (following simple guideline - "no news is good news!"). If not, use custom matchers to verify order properties like I did in the sample above.

Niobium answered 18/12, 2012 at 17:50 Comment(2)
It's not that reference comparison is used, it's that the Equals-implementation is used. So if you override the Equals-method to consider equivalent orders as equal that would also solve the problem.Adan
@PatrikHägne: yep, my mental shortcuts running wild, thanks for reminder.Niobium

© 2022 - 2024 — McMap. All rights reserved.