The Redux architecture we’re using for our application state relies on all our state objects being properly immutable. So far, we’ve relied on nothing more than self-discipline to ensure no mistakes are made. By adding some convention testing to our project, we can enlist some help in avoiding common errors.
As a beginning, let’s declare a simple attribute that we can use to mark each immutable class. We’ll use this attribute to identify the classes that should be immutable and therefore subject to our rules.
In addition to the immutable types we declare ourselves, we can use existing types from the .NET library itself. To make it easy to test whether a type is immutable, let’s define an extension method on
Type that wraps our desired logic.
The logic is relatively simple. If we’ve tagged the type with our new attribute, if it’s a system supplied immutable type, or if it’s a generic type made up of immutable types, then it’s immutable.
Using is object is a tidy way of checking the value isn’t null; I’ve seen this approach emerging in some C# 8.0 code - time will tell whether it becomes a common idiom. I found it odd to read at first, but it’s grown on me.
Not shown, the set
_systemImmutableTypes contains standard .NET types we’ve selected as immutable. Currently those types are
We can now define our first test - that all of the properties on an immutable type should themselves be immutable.
For each property passed into the test, we check that the type of the property is immutable. If not, we fail the test.
Where do those property values come from?
[MemberData] attribute identifies which member, in our case a method, will be used to supply the data.
Starting with [Immutable] on our
WordTutorApplication class, we quickly need to add it to
Screen for this test to pass.
Our next test checks that all these properties are read-only. We don’t want our immutable types to have any writable properties, after all.
Fortunately, all of our existing classes satisfy this test.
Our next test is to ensure that any subclass of an immutable type is itself marked as immutable.
Again we’re using a
[MemberData] attribute to specify where our test data comes from. Finding the subclasses is fairly straightforward:
Lastly, we want all our immutable types to be explicitly abstract or sealed:
What have we achieved here?
We have a set of simple conventions that we want all our immutable types to adhere to. Instead of stating these in a checklist or some other kind of documentation, we’ve embedded the rules into our test suite making their enforcement an active part of our development process.