Following on from our previous post on convention testing we can extend the conventions by considering the standards we want to follow when we write methods on our immutable types.

One of the conventions we’ve been following is the use of methods that create a near clone of the current object, but with a specific change present. We’ve been giving these methods names that start with With - in the functional programming world they’re commonly known as withers.

For our first test, let’s check that the return type of each wither is the same as the declaring type. Enforcing this ensures that we can chain withers together without losing type information as we go.

[Theory]
[MemberData(nameof(FindWithersOfImmutableTypes))]
public void WithersShouldReturnTheirDeclaringType(
    MethodInfo wither)
{
    wither.ReturnType.Should().Be(
        wither.DeclaringType,
        $"wither {wither.Name} of type {wither.DeclaringType.Name} "
        + $"should return {wither.DeclaringType.Name}");
}

We just check that the return type of the method is the same as the declaring type. The because string provides additional information that gets included if the test fails. The goal is to make it as obvious as possible what went wrong so that the fix can be completed as quickly as possible.

To find all the withers that need testing, we iterate over all known immutable types looking for methods with names that start with With:

public static IEnumerable<object[]> FindWithersOfImmutableTypes()
    => from t in GetImmutableTypes()
        from m in t.GetMethods()
        where m.Name.StartsWith("With", StringComparison.Ordinal)
        select new object[] { m };

Good news - all our existing withers satisfy the test.

Now we want to check the parameters we pass to the withers - the parameter names should match the names of the properties that are going to be modified. Using this convention makes the withers more predictable when we’re using them - the parameter names shown in IntelliSense will accurately reflect the changes being made.

[Theory]
[MemberData(nameof(FindWithersOfImmutableTypes))]
public void WitherParametersShouldIdentifyProperties(
    MethodInfo wither)
{
    var properties = (
        from p in wither.DeclaringType.GetProperties()
        select p.Name
    ).ToHashSet(StringComparer.OrdinalIgnoreCase);
    wither.GetParameters().Should().OnlyContain(
        p => properties.Contains(p.Name),
        $"parameters should be one of {string.Join(", ", properties)}");
}

We start by finding all the properties of the declaring type and creating a set of all the names of the public properties. Each of the parameters is then checked to see if the parameter name is present - using a case insensitive check. Again we include a because string to describe what’s expected.

This time around, we have a couple of test failures, both on the class VocabularyBrowserScreen:

Firstly, the method WithSelection() fails because the parameter word needs to be renamed to selection.

public VocabularyBrowserScreen WithSelection(
    VocabularyWord word)

Secondly, the method WithNoSelection() fails because it’s not a wither - it doesn’t have any parameters at all.

public VocabularyBrowserScreen WithNoSelection()

To fix this second test failure, let’s rename the method to ClearSelection() so that it’s not picked up by these tests.

But, we should have some tests for it, surely? Let’s create tests for clearers, with slightly different conventions:

[Theory]
[MemberData(nameof(FindClearersOfImmutableTypes))]
public void ClearersShouldReturnTheirDeclaringType(MethodInfo clearer)
{
    clearer.ReturnType.Should().Be(
        clearer.DeclaringType,
        $"clearer {clearer.Name} of type {clearer.DeclaringType.Name} "
        + $"should return {clearer.DeclaringType.Name}");
}

public static IEnumerable<object[]> FindClearersOfImmutableTypes()
    => from t in GetImmutableTypes()
        from m in t.GetMethods()
        where m.Name.StartsWith("Clear", StringComparison.Ordinal)
        select new object[] { m };

Our first test is essentially identical to the same test for our withers - it might be worth merging the two later on.

Next, we want to check that the name of the clearer identifies the property we’re going to clear.

[Theory]
[MemberData(nameof(FindClearersOfImmutableTypes))]
public void ClearersShouldHaveNamesIdentifyingTheClearedProperty(
    MethodInfo clearer)
{
    var propertyName = clearer.Name.Substring("Clear".Length);
    clearer.DeclaringType.GetProperties()
        .Should().Contain(
            p => p.Name.Equals(
                propertyName, StringComparison.OrdinalIgnoreCase),
            $"no property {propertyName} was found on "
            + $"{clearer.DeclaringType.Name}");
}

These additional conventions will help us construct any new immutable types correctly as we move forward.

What conventions do you have in your projects? How many of those conventions are currently managed by hand? Do you think any of them could be cross-checked by some smart unit tests?

Prior post in this series:
Convention testing for immutable types
Next post in this series:
Logging

Comments

blog comments powered by Disqus