Effective Tests: Expected Objects


Posts In This Series

In the last installment of the Effective Tests series, the topic of Custom Assertions was presented as a strategy for helping to clarify the intent of our tests. This time we’ll take a look at another test pattern for improving the communication of our tests in addition to reducing test code duplication and the need to add test-specific code to our production types.

Expected Objects

Writing tests often involves inspecting the state of collaborating objects and the messages they exchange within a system. This often leads to declaring multiple assertions on fields of the same object which can lead to several maintenance issues. First, if multiple specifications need to verify the same values then this can result in test code duplication. For example, two searches for a customer record with different criteria may be expected to return the same result. Second, when many fine-grained assertions are performed within a specification, the overall purpose can become obscured. For example, a specification may indicate that a value returned from an order process “should contain a first name and a last name and a home phone number and an address line 1 and …” while the intended perspective may be that the operation “should return a shipment confirmation”.

One solution to this problem is to override an object’s equality operators and/or methods to suit the needs of the test. Unfortunately, this is not without its own set of issues. Aside from introducing behavior into the system which is only exercised by the tests, this strategy may conflict with the existing or future needs of the system due to a difference in how each define equality for the objects being compared. While a test may need to compare all the properties of two objects, the system may require equality to be based upon the object’s identity (e.g. two customers are the same if they have the same customer Id). It may happen that the system already defines equality suitable to the needs of the test, but this is subject to change. A system may compare two objects by value for the purposes of indexing, ensuring cardinality, or an assortment of domain-specific reasons whose needs may change as the system evolves. While the initial state of an object’s definition of equality may coincide with the needs of the test, the needs of both represent two axes of change which could lead to higher maintenance costs if not dealt with separately.

When using state-based verification, one way of avoiding test code duplication, obscurity and the need to equip the system with test-specific equality code is to implement the Expected Object pattern. The Expected Object pattern defines objects which encapsulate test-specific equality separate from the objects they are compared against. An expected object may be implemented as a sub-type whose equality members have been overloaded to perform the desired comparisons or as a test-specific type designed to compare itself against another object type.

Consider the following specification which validates that placing an order returns an order receipt populated with the expected values:

[Subject(typeof (OrderService))]
public class when_an_order_is_placed : WithSubject<OrderService>
{
  static readonly Guid CustomerId = new Guid("061F3CED-405F-4261-AF8C-AA2B0694DAD8");
  const long OrderNumber = 1L;
  static Customer _customer;
  static Order _order;
  static OrderReceipt _orderReceipt;


  Establish context = () =>
  {
    _customer = new TestCustomer(CustomerId)
    {
      FirstName = "First",
                LastName = "Last",
                PhoneNumber = "5129130000",
                Address = new Address
                {
                  LineOne = "123 Street",
                  LineTwo = string.Empty,
                  City = "Austin",
                  State = "TX",
                  ZipCode = "78717"
                }
    };
    For<IOrderNumberProvider<long>>().Setup(x => x.GetNext()).Returns(OrderNumber);
    For<ICustomerRepository>().Setup(x => x.Get(Parameter.IsAny<Guid>())).Returns(_customer);
    _order = new Order(1, "Product A");
  };

  Because of = () => _orderReceipt = Subject.PlaceOrder(_order, _customer.Id);

  It should_return_a_receipt_with_order_number = () => _orderReceipt.OrderNumber.ShouldEqual(OrderNumber.ToString());

  It should_return_a_receipt_with_order_description = () => _orderReceipt.Orders.ShouldContain(_order);

  It should_return_a_receipt_with_customer_id = () => _orderReceipt.CustomerId.ShouldEqual(_customer.Id.ToString());

  It should_return_an_order_receipt_with_customer_name = () => _orderReceipt.CustomerName.ShouldEqual(_customer.FirstName + " " + _customer.LastName);

  It should_return_a_receipt_with_customer_phone = () => _orderReceipt.CustomerPhone.ShouldEqual(_customer.PhoneNumber);

  It should_return_a_receipt_with_address_line_1 = () => _orderReceipt.AddressLineOne.ShouldEqual(_customer.Address.LineOne);

  It should_return_a_receipt_with_address_line_2 = () => _orderReceipt.AddressLineTwo.ShouldEqual(_customer.Address.LineTwo);

  It should_return_a_receipt_with_city = () => _orderReceipt.City.ShouldEqual(_customer.Address.City);

  It should_return_a_receipt_with_state = () => _orderReceipt.State.ShouldEqual(_customer.Address.State);

  It should_return_a_receipt_with_zip = () => _orderReceipt.ZipCode.ShouldEqual(_customer.Address.ZipCode);
}
Listing 1

While the specification in listing 1 provides ample detail about the values that should be present on the returned receipt, such an implementation precludes reuse and tends to overwhelm the purpose of the specification. This problem is further compounded as the composition complexity increases.

As an alternative to declaring what each field of a particular object should contain, the Expected Object pattern allows you to declare what a particular object should look like. By replacing the specification’s discrete assertions with a single assertion comparing an Expected Object against a resulting state, the essence of the specification can be preserved while maintaining an equivalent level of verification.

Consider the following simple implementation for an Expected Object:

class ExpectedOrderReceipt : OrderReceipt
{
  public override bool Equals(object obj)
  {
    var otherReceipt = obj as OrderReceipt;

    return OrderNumber.Equals(otherReceipt.OrderNumber) &&
      CustomerId.Equals(otherReceipt.CustomerId) &&
      CustomerName.Equals(otherReceipt.CustomerName) &&
      CustomerPhone.Equals(otherReceipt.CustomerPhone) &&
      AddressLineOne.Equals(otherReceipt.AddressLineOne) &&
      AddressLineTwo.Equals(otherReceipt.AddressLineTwo) &&
      City.Equals(otherReceipt.City) &&
      State.Equals(otherReceipt.State) &&
      ZipCode.Equals(otherReceipt.ZipCode) &&
      Orders.ToList().SequenceEqual(otherReceipt.Orders);
  }
}
Listing 2

Establishing an instance of the expected object in listing 2 allows the previous discrete assertions to be replaced with a single assertion declaring what the returned receipt should look like:

[Subject(typeof (OrderService))]
public class when_an_order_is_placed : WithSubject<OrderService>
{
  const long OrderNumber = 1L;
  static readonly Guid CustomerId = new Guid("061F3CED-405F-4261-AF8C-AA2B0694DAD8");
  static Customer _customer;
  static ExpectedOrderReceipt _expectedOrderReceipt;
  static Order _order;
  static OrderReceipt _orderReceipt;


  Establish context = () =>
  {
    _customer = new TestCustomer(CustomerId)
    {
      FirstName = "First",
                LastName = "Last",
                PhoneNumber = "5129130000",
                Address = new Address
                {
                  LineOne = "123 Street",
                  LineTwo = string.Empty,
                  City = "Austin",
                  State = "TX",
                  ZipCode = "78717"
                }
    };
    For<IOrderNumberProvider<long>>().Setup(x => x.GetNext()).Returns(OrderNumber);
    For<ICustomerRepository>().Setup(x => x.Get(Parameter.IsAny<Guid>())).Returns(_customer);
    _order = new Order(1, "Product A");

    _expectedOrderReceipt = new ExpectedOrderReceipt
    {
      OrderNumber = OrderNumber.ToString(),
                  CustomerName = "First Last",
                  CustomerPhone = "5129130000",
                  AddressLineOne = "123 Street",
                  AddressLineTwo = string.Empty,
                  City = "Austin",
                  State = "TX",
                  ZipCode = "78717",
                  CustomerId = CustomerId.ToString(),
                  Orders = new List<Order> {_order}
    };
  };

  Because of = () => _orderReceipt = Subject.PlaceOrder(_order, _customer.Id);

  It should_return_an_receipt_with_shipping_information_and_order_number =
    () => _expectedOrderReceipt.Equals(_orderReceipt).ShouldBeTrue();
}
Listing 3

The implementation strategy in listing 3 offers a subtle shift in perspective, but one which may more closely model the language of the business.

This is not to say that discrete assertions are always wrong. The level of detail modeled by an application’s specifications should be based upon the needs of the business. Consider the test runner output for both implementations:

ExpectedObjectContrast

Figure 1

Examining the results of executing both specifications in figure 1, we see that the first describes each field being validated, while the second describes what the validations of these fields collectively mean. Which is best will depend upon your particular business needs. While the first implementation provides a more detailed specification of the receipt, this may or may not be as important to the business as knowing that the receipt as a whole is correct. For example, consider if the order number were missing. Is the correct perspective that the receipt is 90% correct or 100% wrong? The correct answer is … it depends.

Explicit Feedback

While the Expected Object implementation shown in listing 2 may be an adequate approach in some cases, it does have the shortcoming of not providing explicit feedback of how the two objects differ. To address this, we can implement our Expected Object as a Custom Assertion. Instead of asserting on the return value of comparing the expected object to an object returned from our system, we can design the Expected Object to throw an exception detailing what state differed between the two objects. The following listing demonstrates this approach:

class ExpectedOrderReceipt : OrderReceipt
{
  public void ShouldEqual(object obj)
  {
    var otherReceipt = obj as OrderReceipt;
    var messages = new List<string>();

    if (!OrderNumber.Equals(otherReceipt.OrderNumber))
      messages.Add(string.Format("For OrderNumber, expected '{0}' but found '{1}'", OrderNumber, otherReceipt.OrderNumber));

    if (!CustomerId.Equals(otherReceipt.CustomerId))
      messages.Add(string.Format("For CustomerId, expected '{0}' but found '{1}'", CustomerId, otherReceipt.CustomerId));

    if (!CustomerName.Equals(otherReceipt.CustomerName))
      messages.Add(string.Format("For CustomerName, expected '{0}' but found '{1}'", CustomerName, otherReceipt.CustomerName));

    if (!CustomerPhone.Equals(otherReceipt.CustomerPhone))
      messages.Add(string.Format("For CustomerPhone, expected '{0}' but found '{1}'", CustomerPhone, otherReceipt.CustomerPhone));

    if (!AddressLineOne.Equals(otherReceipt.AddressLineOne))
      messages.Add(string.Format("For AddressLineOne, expected '{0}' but found '{1}'", AddressLineOne, otherReceipt.AddressLineOne));

    if (!AddressLineTwo.Equals(otherReceipt.AddressLineTwo))
      messages.Add(string.Format("For AddressLineTwo, expected '{0}' but found '{1}'", AddressLineTwo, otherReceipt.AddressLineOne));

    if (!City.Equals(otherReceipt.City))
      messages.Add(string.Format("For City, expected '{0}' but found '{1}'", City, otherReceipt.City));

    if (!State.Equals(otherReceipt.State))
      messages.Add(string.Format("For State, expected '{0}' but found '{1}'", State, otherReceipt.State));

    if (!ZipCode.Equals(otherReceipt.ZipCode))
      messages.Add(string.Format("For ZipCode, expected '{0}' but found '{1}'", ZipCode, otherReceipt.ZipCode));

    if (!Orders.ToList().SequenceEqual(otherReceipt.Orders))
      messages.Add("For Orders, expected the same sequence but was different.");

    if(messages.Count > 0)
      throw new Exception(string.Join(Environment.NewLine, messages));
  }
}
Listing 4

The following listing shows the specification modified to use the new Expected Object implementation with several values on the TestCustomer modified to return values differing from the expected value:

[Subject(typeof (OrderService))]
public class when_an_order_is_placed : WithSubject<OrderService>
{
  const long OrderNumber = 1L;
  static readonly Guid CustomerId = new Guid("061F3CED-405F-4261-AF8C-AA2B0694DAD8");
  static Customer _customer;
  static ExpectedOrderReceipt _expectedOrderReceipt;
  static Order _order;
  static OrderReceipt _orderReceipt;


  Establish context = () =>
  {
    _customer = new TestCustomer(CustomerId)
    {
      FirstName = "Wrong",
                LastName = "Wrong",
                PhoneNumber = "Wrong",
                Address = new Address
                {
                  LineOne = "Wrong",
                  LineTwo = "Wrong",
                  City = "Austin",
                  State = "TX",
                  ZipCode = "78717"
                }
    };
    For<IOrderNumberProvider<long>>().Setup(x => x.GetNext()).Returns(OrderNumber);
    For<ICustomerRepository>().Setup(x => x.Get(Parameter.IsAny<Guid>())).Returns(_customer);
    _order = new Order(1, "Product A");

    _expectedOrderReceipt = new ExpectedOrderReceipt
    {
      OrderNumber = OrderNumber.ToString(),
                  CustomerName = "First Last",
                  CustomerPhone = "5129130000",
                  AddressLineOne = "123 Street",
                  AddressLineTwo = string.Empty,
                  City = "Austin",
                  State = "TX",
                  ZipCode = "78717",
                  CustomerId = CustomerId.ToString(),
                  Orders = new List<Order> {_order}
    };
  };

  Because of = () => _orderReceipt = Subject.PlaceOrder(_order, _customer.Id);

  It should_return_an_receipt_with_shipping_and_order_information = () => _expectedOrderReceipt.ShouldEqual(_orderReceipt);
}
Listing 5

Running the specification produces the following output:

ExpectedObjectExplicitFeedback
Figure 2

Conclusion

This time, we took a look at the Expected Object pattern which aids in reducing code duplication, eliminating the need to put test-specific equality behavior in our production code and serves as a strategy for further clarifying the intent of our specifications. Next time, we’ll look at some strategies for combating obscurity and test-code duplication caused by test data.

Effective Tests: Custom Assertions