Introducing the Expected Objects Library

Introduced in the Effective Test Series, the Expected Object pattern is a technique involving the encapsulation of test-specific logic within a specialized type designed to compare its configured state against that of another object. Use of the Expected Object pattern eliminates the need to encumber system objects with test-specific equality behavior, helps to reduce test code duplication and can aid in expressing the logical intent of automated tests.

While the Expected Object pattern is a great strategy for helping adhere to good testing practices, the process of actually implementing the required types can be less than motivating. To alleviate the burden of hand-rolling Expected Object types, I created the Expected Objects library. This library provides the ability to compare the state of one object against another without relying upon the provided type’s equality members. In addition to the ability to assert equality, the library also provides equality assertion methods which provide feedback of how each member of an object differs from an expected state.

The following examples demonstrate the capabilities of the library:

 

Comparing Flat Objects

public class when_retrieving_a_customer
{
	static Customer _actual;
	static ExpectedObject _expected;

	Establish context = () =>
		{
			_expected = new Customer
				            {
				            	Name = "Jane Doe",
				            	PhoneNumber = "5128651000"
				            }.ToExpectedObject();

			_actual = new Customer
				          {
				          	Name = "John Doe",
				          	PhoneNumber = "5128654242"
				          };
		};

	It should_return_the_expected_customer = () => _expected.ShouldEqual(_actual);
}



class Customer
{
	public string Name { get; set; }
	public string PhoneNumber { get; set; }
}

Results:

should return the expected customer : Failed For Customer.Name, expected "Jane Doe" but found "John Doe". For Customer.PhoneNumber, expected "5128651000" but found "5128654242".

 

Comparing Composed Objects

public class when_retrieving_a_customer_with_address
{
	static Customer _actual;
	static ExpectedObject _expected;

	Establish context = () =>
		{
			_expected = new Customer
				            {
				            	Name = "Jane Doe",
				            	PhoneNumber = "5128651000",
				            	Address = new Address
				            		          {
				            		          	AddressLineOne = "123 Street",
				            		          	AddressLineTwo = string.Empty,
				            		          	City = "Austin",
				            		          	State = "TX",
				            		          	Zipcode = "78717"
				            		          }
				            }.ToExpectedObject();

			_actual = new Customer
				          {
				          	Name = "John Doe",
				          	PhoneNumber = "5128654242",
				          	Address = new Address
				          		          {
				          		          	AddressLineOne = "456 Street",
				          		          	AddressLineTwo = "Apt. 3",
				          		          	City = "Waco",
				          		          	State = "TX",
				          		          	Zipcode = "76701"
				          		          }
				          };
		};

	It should_return_the_expected_customer = () => _expected.ShouldEqual(_actual);
}



class Customer
{
	public string Name { get; set; }
	public string PhoneNumber { get; set; }
	public Address Address { get; set; }
}



class Address
{
	public string AddressLineOne { get; set; }
	public string AddressLineTwo { get; set; }
	public string City { get; set; }
	public string State { get; set; }
	public string Zipcode { get; set; }
}

Results:

should return the expected customer : Failed For Customer.Name, expected "Jane Doe" but found "John Doe". For Customer.PhoneNumber, expected "5128651000" but found "5128654242". For Customer.Address.AddressLineOne, expected "123 Street" but found "456 Street". For Customer.Address.AddressLineTwo, expected "" but found "Apt. 3". For Customer.Address.City, expected "Austin" but found "Waco". For Customer.Address.Zipcode, expected "78717" but found "76701".

 

Comparing Collections

public class when_retrieving_a_collection_of_customers
{
	static List<Customer> _actual;
	static ExpectedObject _expected;

	Establish context = () =>
		{
			_expected = new List<Customer>
				            {
				            	new Customer {Name = "Customer A"},
				            	new Customer {Name = "Customer B"}
				            }.ToExpectedObject();

			_actual = new List<Customer>
				          {
				          	new Customer {Name = "Customer A"},
				          	new Customer {Name = "Customer C"}
				          };
		};

	It should_return_the_expected_customers = () => _expected.ShouldEqual(_actual);
}

Results:

should return the expected customers : Failed For List`1[1].Name, expected "Customer B" but found "Customer C".

 

Comparing Dictionaries

public class when_retrieving_a_dictionary
{
	static IDictionary<string, string> _actual;
	static IDictionary<string, string> _expected;

	static bool _result;

	Establish context = () =>
		{
			_expected = new Dictionary<string, string> {{"key1", "value1"}};
			_actual = new Dictionary<string, string> {{"key1", "value1"}, {"key2", "value2"}};
		};

	It should_return_the_expected_dictionary = () => _expected.ToExpectedObject().ShouldEqual(_actual);
}

Results:

should return the expected dictionary : Failed For Dictionary`2[1], expected nothing but found [[key2, value2]].

 

Comparing Types with Indexes

public class when_retrieving_a_type_with_an_index
{
	static IndexType _actual;
	static IndexType _expected;

	static bool _result;

	Establish context = () =>
		{
			_expected = new IndexType(new List<int> {1, 2, 3, 4, 6});
			_actual = new IndexType(new List<int> {1, 2, 3, 4, 5});
		};

	It should_return_the_expected_type = () => _expected.ToExpectedObject().ShouldEqual(_actual);
}



class IndexType
{
	readonly IList<T> _ints;

	public IndexType(IList<T> ints)
	{
		_ints = ints;
	}

	public T this[int index]
	{
		get { return _ints[index]; }
	}

	public int Count
	{
		get { return _ints.Count; }
	}
}

Results:

should return the expected type : Failed For IndexType`1.Item[4], expected [6] but found [5].

 

Comparing Partial Objects

public class when_retrieving_a_customer
{
	static Customer _actual;
	static ExpectedObject _expected;

	Establish context = () =>
		{
			_expected = new
				            {
				            	Name = "Jane Doe",
				            	Address = new
				            		          {
				            		          	City = "Austin"
				            		          }
				            }.ToExpectedObject();

			_actual = new Customer
				          {
				          	Name = "John Doe",
				          	PhoneNumber = "5128654242",
				          	Address = new Address
				          		          {
				          		          	AddressLineOne = "456 Street",
				          		          	AddressLineTwo = "Apt. 3",
				          		          	City = "Waco",
				          		          	State = "TX",
				          		          	Zipcode = "76701"
				          		          }
				          };
		};

	It should_have_the_correct_name_and_address = () => _expected.ShouldMatch(_actual);
}

Results:

should have the correct name and address : Failed For Customer.Name, expected "Jane Doe" but found "John Doe". For Customer.Address.City, expected "Austin" but found "Waco".

 

Extensibility

The Expected Objects library is extensible, so if it doesn’t provide the exact comparison strategies you need then you’re free to add our own.

The main extensibility point is the IComparisonStrategy which is declared as follows:

public interface IComparisonStrategy
{
    bool CanCompare(Type type);
    bool AreEqual(object expected, object actual, IComparisonContext comparisonContext);
}

To register a custom strategy, simply call the Configure() method and use the supplied ConfigurationContext to call the PushStrategy<t>() method:

_expected = new Foo("Bar")
	.ToExpectedObject()
	.Configure(ctx => ctx.PushStrategy<FooComparisonStrategy>());

This will push the custom strategy onto the stack used by the Expected Objects library during its comparisons.

Custom Comparison Strategy Example

The following demonstrates how the Expected Objects library could be extended to compare an expected object to the contents of a Web page.

Consider the following specification:

public class when_displaying_the_customer_view
{
	static Mock<IWebDriver> _actual;
	static ExpectedObject _expected;

	Establish context = () =>
		{
			var nameElementStub = new Mock<IWebElement>();
			nameElementStub.Setup(x => x.Text).Returns("Jane Doe");
			var addressElementStub = new Mock<IWebElement>();
			addressElementStub.Setup(x => x.Text).Returns("456 Street");
			var buttonElementStub = new Mock<IWebElement>();
			buttonElementStub.Setup(x => x.Text).Returns("Cancel");
			_actual = new Mock<IWebDriver>();
			_actual.Setup(x => x.FindElement(By.Id("name"))).Returns(nameElementStub.Object);
			_actual.Setup(x => x.FindElement(By.CssSelector("input[name='address']"))).Returns(addressElementStub.Object);
			_actual.Setup(x => x.FindElement(By.XPath("//input[@value='submit']"))).Returns(buttonElementStub.Object);

			_expected = new ExpectedView()
				.WithId("name", "John Doe")
				.WithCssSelector("input[name='address']", "123 Street")
				.WithXPath("//input[@value='submit']", "Submit")
				.ToExpectedObject()
				.Configure(ctx =>
					{
						ctx.PushStrategy<ExpecedViewComparisonStrategy>();
						ctx.IgnoreTypes();
					});
		};

	It should_display_the_expected_view = () => _expected.ShouldEqual(_actual.Object);
}

Here, the Selenium 2 IWebDriver type is being stubbed to emulate an active Selenium testing session. Next, a custom ExpectedView type is instantiated and configured to expect one value to be located by an Id, one by a CSS Selector and one by an XPath. Lastly, the expected object is compared to the actual object (in this case, the IWebDriver stub).

Executing the specification produces the following results:

should display the expected view : Failed For IWebDriverProxy.FindElement(By.Id("name")), expected "John Doe" but found "Jane Doe". For IWebDriverProxy.FindElement(By.CssSelector("input[name='address']")), expected "123 Street" but found "456 Street". For IWebDriverProxy.FindElement(By.XPath("//input[@value='submit']")), expected "Submit" but found "Cancel".

 

Here is the ExpectedView and ExpectedViewComparisonStrategy implementation:

class ExpectedView
{
	public ExpectedView()
	{
		Ids = new List<Tuple<string, string>>();
		CssSelectors = new List<Tuple<string, string>>();
		XPaths = new List<Tuple<string, string>>();
	}

	public List<Tuple<string, string>> Ids { get; private set; }
	public List<Tuple<string, string>> CssSelectors { get; private set; }
	public List<Tuple<string, string>> XPaths { get; private set; }

	public ExpectedView WithId(string name, string value)
	{
		Ids.Add(new Tuple<string, string>(name, value));
		return this;
	}

	public ExpectedView WithCssSelector(string selector, string value)
	{
		CssSelectors.Add(new Tuple<string, string>(selector, value));
		return this;
	}

	public ExpectedView WithXPath(string path, string value)
	{
		XPaths.Add(new Tuple<string, string>(path, value));
		return this;
	}
}



class ExpecedViewComparisonStrategy : IComparisonStrategy
{
	public bool CanCompare(Type type)
	{
		return typeof (ExpectedView).IsAssignableFrom(type);
	}

	public bool AreEqual(object expected, object actual, IComparisonContext comparisonContext)
	{
		bool areEqual = true;
		var view = (ExpectedView) expected;
		var driver = (IWebDriver) actual;
		view.Ids.ForEach(id => areEqual = CompareIds(driver, id, comparisonContext) && areEqual);
		view.CssSelectors.ForEach(selector => areEqual = CompareCssSelectors(driver, selector, comparisonContext) && areEqual);
		view.XPaths.ForEach(path => areEqual = CompareXPaths(driver, path, comparisonContext) && areEqual);
		return areEqual;
	}

	static bool CompareIds(IWebDriver driver, Tuple<string, string> expected, IComparisonContext comparisonContext)
	{
		bool areEqual = true;
		IWebElement idElement = driver.FindElement(By.Id(expected.Item1));
		areEqual = comparisonContext.AreEqual(expected.Item2, idElement.Text, "FindElement(By.Id(\"" + expected.Item1 + "\"))") && areEqual;
		return areEqual;
	}

	static bool CompareCssSelectors(IWebDriver driver, Tuple<string, string> expected, IComparisonContext comparisonContext)
	{
		bool areEqual = true;
		IWebElement idElement = driver.FindElement(By.CssSelector(expected.Item1));
		areEqual = comparisonContext.AreEqual(expected.Item2, idElement.Text, "FindElement(By.CssSelector(\"" + expected.Item1 + "\"))") && areEqual;
		return areEqual;
	}

	static bool CompareXPaths(IWebDriver driver, Tuple<string, string> expected, IComparisonContext comparisonContext)
	{
		bool areEqual = true;
		IWebElement idElement = driver.FindElement(By.XPath(expected.Item1));
		areEqual = comparisonContext.AreEqual(expected.Item2, idElement.Text, "FindElement(By.XPath(\"" + expected.Item1 + "\"))") && areEqual;
		return areEqual;
	}
}

Note: This example is for demonstration purposes only.

 

Conclusion

The Expected Objects library is published as a NuGet package and the source is hosted on github. Feel free to provide feedback.

About Derek Greer

Derek Greer is a consultant, aspiring software craftsman and agile enthusiast currently specializing in C# development on the .Net platform.
This entry was posted in Uncategorized and tagged , . Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #883

  • Kevin Gurton

    I’m not sure that the EqualsOverrideComparisonStrategy should be used by default. If you’re overriding Equals, and want to use that for your test, why not just use Assert.AreEqual(expected, actual)?

    • Derek Greer

      Expected Objects does a recursive comparison, so while these strategies might not make sense for the top level object, they are necessary for comparing composed types.

  • Robert Simmons

    Hehe, I wrote something similar (although not as in-depth as what you’ve done, since we didn’t need it) for our in-house testing about a year ago. I wasn’t a fan of overriding Equals everywhere either.

  • fschwiet

    Interesting…  I wonder if you’ve used this in such a way that the test only needs to define the fields on the object that the test cares about?  In this case I would probably just use per-field assertions, but curious.

    • Anonymous

      Yes, that’s what the “Comparing Partial Objects” example demonstrates.  Writing assertions for each field is fine if the domain your validating is an API (e.g. “The service response should have these fields”), but they tend to lead to obscure tests when modeling business concerns (e.g. “When the customer does X, the system should do Y”).

  • Anonymous

    Derek, Thanks SO MUCH for creating this tool. I and my entire development team uses Expected Objects every day in creating a new virtual terminal and payment processing solution (fairly large project). It has been ideal in so many ways. We have found that our test count has decreased drastically thereby making our test suite drastically more maintainable. ExpectedObjects RULE!