Saturday, June 11, 2016

ValueTypeAssertions

The Problem

Primitive Obsession is one of the most pervasive code smells out there. You can address it by moving a primitive in to a simple class.


I call the resulting class a "value type", but don't confuse it with C#'s notion of a value type, which doesn't get its own heap allocation, and is passed-by-value to other methods, and is a source of bugs if it's mutable. I mean "a type that represent a value in some domain".
If you want to implement equality on that class, there are a lot of tricky details that are easy to get wrong, at least in C#. For example:


This will throw an exception when trying to cast the Bar to a Point. So you try to fix it:


This will throw when trying to call null.GetType(). Uggh.

You probably want to override operator==() as well.


The compiler tells you to implement operator!=() to go with it, so you copy/paste and change the method name:


Oops, you forgot to negate the check. Bug.

If the value in question is a case-insensitive identifier of some sort, it's important that the GetHashCode() is implemented correctly. Don't do this:


Maybe you want to implement IEquatable<>, too, and you better get these details right there, too.

Many programmers don't test these details at all, or they test a few but not all, and they have to repeat the same set of tests each time they introduce a new class. If you discover a new rule (ToString() should follow equality, right?) you have to update all the tests.

Prior Art

Assertion libraries typically have an equality assertion. For example, in NUnit:

    Assert.AreEqual( new Point(7,8), new Point(7,8) );

That is insufficient. It only tells you that one of the equality checks you've written is correct, and doesn't catch all the other cases listed above.

The Solution

ValueTypeAssertions addresses all the mistakes I have ever made, or seen made, or can imagine when implementing equality in C#. Grab it from NuGet, and write a unit test like this:

    ValueTypeAssertions.HasValueEquality(new NtfsPath("foo.txt"), new NtfsPath("foo.txt"));


This says "these two objects should equal, in every way that C# recognizes equality".

  ValueTypeAssertions.HasValueInequality(new NtfsPath("foo.txt"), new NtfsPath("bar.txt"));


Which says the same thing about not being equal.

If some part of your value should be case insensitive, just add another assertion:

  ValueTypeAssertions.HasValueEquality(new NtfsPath("foo.txt"), new NtfsPath("FOO.TXT"));

If you wrap two values, assert the combinations:

  ValueTypeAssertions.HasValueInequality(new Point(1, 2), new Point(1, 8));
  ValueTypeAssertions.HasValueInequality(new Point(1, 2), new Point(0, 2));

You can find the source on GitHub.

Feedback

Do you find this useful?

What change would make it more useful to you?

Is there a name for this that would be more obvious?

No comments: