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 .Equals()
and .GetHashCode()
.
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.
For 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 .GetHashCode()
, using _comparer
to generate a value based on the message itself.
For AggregateResult
, the code is substantially similar - with the twist that the implemenations of .Equals(AggregateResult)
and .GetHashCode()
need to combine results from all the contained ValidationResults
.
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 ??
operator.
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: 1100_1111_1001_0000_0000_0111_1001
.
For SuccessResult
, things are trivially simple - since every success is equal to every other, the code has almost nothing to do:
Comments
blog comments powered by Disqus