The Resemblance and Likeness patterns improve the readability and diagnosability of developer tests.
I’ve been using the Resemblance and Likeness patterns for several years to clarify my developer tests. They’ve earned a permanent spot in my toolbox and I use them frequently. Thanks to Mark Seemann for introducing the patterns!
Here I’ll explain the patterns and they value they deliver.
Resemblance Example
Consider this common scenario: saving some data and alerting subscribers about the new data.
This scenario contains at two expectations: The saved data should be retrievable as saved and that a notification was sent.
The immediate intuition would be to test the scenario with separate assertions for each expectation.
|
|
But multiple assertions lead to problems. The test’s error message will only include information from the first failing assertion.
In the previous example, if the retrieved data is incorrect, then we don’t know if the notification was sent.
|
|
Flipping the order doesn’t solve the problem, it just changes what information we might miss out on.
The Resemblance pattern creates a data structure with the whole context of what’s expected from a developer test. Collecting all expectations into one data structure allows a single assertion with all the context we might want about the final test state.
Here’s a relatively complete example including a faked dependency.
|
|
Likeness Example
The likeness pattern provides a similar value. It standardizes an unruly comparison into a single operation. However, the purpose is different. Resemblance focuses on collecting the whole context of test expectations but likeness focuses on simplifying comparision for data that’s awkward to compare.
A simple example is collections. I often write a simple likeness to standardize sneaky assumptions about collections, like the sort order.
|
|
Sorting by Id may seem too simple for a separate method, but the method communicates the intent behind the sort and centralizes changes if the comparison becomes more sophisticated in the future.
For example, the expected and returned data structures might not be the same, but we still want to compare them in some way
Consider this simple online order example.
|
|
We might want to test that every OrderRequest we place results in a ConfirmedOrder for the same customer, address, etc.
We can simplify this comparison with a likeness
|
|
Likenesses can often be reused across many tests too.
Ad-hoc likeness structures
Creating dedicated types (like OrderDestinationLikeness
) may still feel too heavy for simple comparisions. For simple comparisions, I often use ad-hoc structures like a tuple or string.
|
|
C#’s positional records also provide a concise syntax for creating likeness types.
|
|
Records, tuples, and strings work really well for resemblances and likenesses because they use value-based equality. If the structures contain the same values, they’re considered equal (as opposed to reference-based equality that check if the object is the same instance). F# users can similarly use anonymous records. Regular C# classes can still be used in a resemblance or likeness when compared with a deep comparision library.
Likenesses to build Resemblances
I should note that this approach to likeness using standard data structures (as opposed to .Equals
overrides) may not be what Mark originally intended, but it seems to be in the same spirit of simplified comparision.
Plus, this data-based approach has a distinct advantage: the data can be used for more than direct comparision. For example, the data can be used to build resemblances. Unlike a boolean equality, the data structures retain their full context when compared in the resemblance or displayed in the test error message.
Likenesses can also optimize their data output for additional outcomes, like improving the readability of test error messages.
Conclusion
The Resemblance and Likeness patterns improve test readability and error diagnosis by normalizing all the factors of a test assertion into a data structure. These normalized data structures highlight the author’s understanding of the factors considered when evaluating test success. This improves code clarity and ensures the test error includes the full failure context. This greatly simplifies error diagnosis, especially for the most time-consuming errors like flaky tests or environment-based test failures.