While writing tests for the code presented in the last couple of weeks, I discovered a notable bug caused by an ommission in the code. If you’re a regular reader of this blog, you may have already spotted what was left out.
Frankly, the omission is a bit embarassing, as it concerns a topic on which I recently wrote an entire series of posts … but there’s a separate lesson here that’s worth sharing. Unit testing works. (Even when you’re testing simple things you won’t ever get wrong!)
Here are the unit tests I wrote that revealed the problem:
Some background: I’m using the xUnit test framework along with Fluent Assertions to check the results. I group tests of the same piece of functionality together in a single class as described by Phil Haack.
The first test checks that a new
AggregateResults containing a single error contains the error I expect. It passed.
The second test checks that including the same error multiple times doesn’t result in the error being duplicated (this property of uniquing is very useful). It also passed.
The third test checks that including identical errors multiple times doesn’t result in the error being duplicated. It failed.
The source of the problem, of course, is that I haven’t implemented equality on
ValidationResult (and its subclasses), so the system is falling back on the default implementation inherited from object, which uses reference equality.
To address this, we modify
ValidationResult by declaring that it implements the interface
IEquatable<ValidationResult>, and by providing a framework for implementation of
Equals() by its subclasses, as follows.
The static member
_comparer is a simple way to ensure that all of our implementations are consistent in the way they treat character comparisons, helping to avoid any issues due to inconsistency between
We include a static
.Equals() method, allowing easy comparison when either instance might already be null:
Our subclasses are forced to implement equality by marking the non-static
.Equals() method abstract - this helps to force those subclasses to consider the issues around equality.
As a convenience, we implement
.Equals(object) ourselves - and we seal the implementation. This has two important effects - it reduces the amount of code required for each subclass, reducing the effort of implementation, and it reduces the opportunity for inconsistency by ensuring that the entire class heirarchy has the same approach. This is a form of defensive programming.
As you probably expect, the implementations on the subclasses are fairly straightforward. For clarity, each also implements
IEquatable<T> for the specific subclass.
ErrorResult, the important check is whether the two errors have the same message:
The more generic method,
.Equals(ValidationResult) (below), simply delegates to the more specific method,
.Equals(ErrorResult) (above). By avoiding duplication of the actual equality comparison, we avoid potential issues with inconsistency if things ever change.
We also mustn’t forget to override
_comparer to generate a value based on the message itself.
AggregateResult, the code is substantially similar - with the twist that the implemenations of
.GetHashCode() need to combine results from all the contained
We’re lucky that
ImmutableHashSet<T> includes the very useful
.SetEquals() method that allows us to easily compare the
other set of results with our own. If
other is null, we fall back to returning false courtesy of the
To calculate the hashcode, we start with a hard coded random prime number and XOR the hashcodes of the various results contained. We don’t do any multiplication because the iteration order for a set isn’t fixed - and we need two
AggregateResults with the same content to have the same hashcode even if they happen to iterate the set in different orders. (This can happen if the items in the two sets were added in different orders.)
There’s nothing special about the number
217 645 177, I just chose a random prime. The underscores (
_) are a new feature in C#7 to improve the readability of numeric constants; it’s espececially useful for binary numbers - here’s the same prime number, to illustrate:
SuccessResult, things are trivially simple - since every success is equal to every other, the code has almost nothing to do: