Use Collection Initializers To Simplify Tedious Mapping and Validation Tests

TL;DR: With custom collection initializers, you can avoid writing repetitive test methods for mappers, converters and validators by using parameterized tests with type-safe yet generic test samples based on expressions and corresponding values.

Shut up and show me the code.

Tests For Mappers etc. Can Be Written Concisely With Test Samples

This may be a niche post, but I have already successfully used the technique described here twice in different contexts for different clients. Also, I enjoyed playing with the C# syntax nonetheless, so here we go.

This post takes the approach of a previous post named “Improve your parameterized Tests with named ValueTuples” and turns it up to 11.

In the past on several occasions, I found myself writing repetitive unit tests for mappers, converters and validators.

<mid-introduction-rant ignore="at-your-leisure">
I have the strong believe mappers and converters need to be tested — most likely at the unit test level — even if the implementation uses Automapper, FluentValidation or something similar. I have seen too many Automapper configurations grow more complex as time went by (which is considered wrong usage by the author of automapper) and then break because of this complexity; not to mention breaking API changes of Automapper itself (looking at you, version 9.0).
</mid-introduction-rant>

A while ago I was writing unit tests for validators. Those tests tend to be repetitive, especially the ones that test single property validation. Traditionally you would end up with two parameterized tests per property, one for the valid case, testing some valid values and one for the invalid case, testing some invalid values. If you have 10 properties, that is 20 almost identical unit tests plus 20 sample declarations. And that is just for one validated class. I was looking for a shorter way with less code duplication that would still be type-safe as well as easily changeable and amendable.

Usually, validated properties are of different types, so we cannot just use the named value tuple technique described in an earlier blog post and declare the invalid value samples as (Action<ValidatedType, string> SetValue, string[] InvalidValues)[] with a bit of SelectMany to create the final test cases. We need something more powerful.

Collection Initializers Are An Overseen Feature Of C#

A not-so-well-known feature of C# are collection initializers. Even the documentation is vague on how to use them on your own types, so I will quickly elaborate on the most important aspects.

The same convention built-in types use that lets you write new List<int> { 1, 2, 3 } instead of writing Add three times, is available for your own custom types. For this to become available, your type must implement IEnumerable (yes, the non-generic one) and provide a method called Add. The signature of this Add method determines what you can write in the collection initializer.

For example, you could do this:

class CollectionInitializerExample : IEnumerable
{
    private readonly List<int> ints = new List<int>();

    public void Add(int anInt) => ints.Add(anInt);

    public IEnumerator GetEnumerator() => ((IEnumerable)ints).GetEnumerator();
}

var example = new CollectionInitializerExample { 1, 2, 3 };

You can also have more than one add method and use all of them in the same collection intializer:

class CollectionInitializerExample : IEnumerable
{
    private readonly List<int> ints = new List<int>();
    private readonly List<string> strings = new List<string>();

    public void Add(int anInt) => ints.Add(anInt);
    public void Add(string aString) => strings.Add(aString);

    public IEnumerator GetEnumerator() => ((IEnumerable)ints.Cast<object>().Concat(strings)).GetEnumerator();
}

var mixedSample = new CollectionInitializerExample { 1, "hello", 2, "world" };

// type-safety makes this invalid
var invalidSample = new CollectionInitializerExample { 1.0 };

You can even have a generic Add method, which is where the real fun begins:

class CollectionInitializerExample : IEnumerable
{
    private readonly List<(object, object)> pairsOfSameType = new List<(object, object)>();

    public void Add<T>(T first, T second) => pairsOfSameType.Add((first, second));

    public IEnumerator GetEnumerator() => ((IEnumerable)pairsOfSameType).GetEnumerator();
}

var pairSample = new CollectionInitializerExample
{
    { 1, 2 },
    { "foo", "bar" },
    { DateTime.Now, DateTime.UtcNow }
};

// type-safety makes this invalid
var invalidSample = new CollectionInitializerExample
{
    { 1, "foo" }
};

And just for fun, pushing C# syntax a bit more, you can declare an Add method with a params argument:

class CollectionInitializerExample : IEnumerable
{
    private readonly List<IEnumerable<object>> arraysOfSameType = new List<IEnumerable<object>>();

    public void Add<T>(params T[] valuesOfSameType) => arraysOfSameType.Add(valuesOfSameType.Cast<object>());

    public IEnumerator GetEnumerator() => ((IEnumerable)arraysOfSameType).GetEnumerator();
}

var paramsSample = new CollectionInitializerExample
{
    { 1, 2, 3, 4 },
    { "foo", "bar" },
    { DateTime.Now, DateTime.UtcNow, DateTime.Today }
};

As cool as this is, I would think twice about using all-too-clever collection initializers in production code. Intellisense does not like them and will autofill nonsense and compiler errors on invalid initializations can become cryptical quickly. However, different rules may apply for simple test code, especially when it becomes a pattern enabling you to succinctly declare test samples for a parameterized test.

Collection Initializers Enable Type-Safe Generic Test Samples

We are now equipped with everything we need to design a class holding test samples for our validator tests:

public class MemberMultiSamples<T> : IEnumerable<(PropertyInfo PropertyInfo, object[] Values)>
{
    private readonly List<(PropertyInfo PropertyInfo, object[] Values)> samples =
        new List<(PropertyInfo PropertyInfo, object[] Values)>();

    public void Add<TValue>(
        Expression<Func<T, TValue>> propertyExpr,
        TValue value,
        params TValue[] additionalValues)
    {
        var propertyInfo = (propertyExpr.Body as MemberExpression)?.Member as PropertyInfo
            ?? throw new ArgumentException(
                $"{nameof(propertyExpr)} must be property expression, but was {propertyExpr}");

        samples.Add((propertyInfo, new object[] { value }.Concat(additionalValues.OfType<object>()).ToArray()));
    }

    public IEnumerator<(PropertyInfo PropertyInfo, object[] Values)> GetEnumerator() =>
        ((IEnumerable<(PropertyInfo PropertyInfo, object[] Values)>)samples).GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)samples).GetEnumerator();
}

The generic Add method forces type safety and allows arbitrarily many sample values, but at least one.

MemberMultiSamples is designed to be test-framework agnostic and usage agnostic, but able to provide the information to set up the test data and generate a nice test display name in the testing framework of your choice. For the NUnit framework and usage as validator tests, the ToValidationTestCaseData extension method could look like this:

public static IEnumerable<TestCaseData> ToValidationTestCaseData<T>(this MemberMultiSamples<T> samples) =>
    samples.SelectMany(s =>
        s.Values.Select(v =>
            new TestCaseData(
                (Action<T, object>)((instance, value) => s.PropertyInfo.SetValue(instance, value)),
                v)
            .SetArgDisplayNames(
                s.PropertyInfo.Name,
                v?.ToString() ?? "<null>")));

This will generate one test case per invalid value with the tested property and the invalid value both in the test case name. It will also create an Action<T, object> for setting the sample value, thus keeping reflection code out of the test.

The following is now all the code necessary to test three different properties with 15 invalid sample values in total. With more properties and more sample values, the conciseness will scale even more.

[Test]
[TestCaseSource(nameof(InvalidSamples))]
public void IsValid_When_individual_values_are_invalid_Then_is_invalid(
    Action<Organisation, object> setValue,
    object invalidValue)
{
    var organisation = new Organisation { Name = "Valid Name", Registration = "RN-33-X", FoundedAtDate = new DateTime(2020, 1, 1) };
    setValue(organisation, invalidValue);

    bool isValid = validator.IsValid(organisation);

    Assert.That(isValid, Is.False);
}

static readonly IEnumerable<TestCaseData> InvalidSamples = new MemberMultiSamples<Organisation>
{
    { o => o.Name, null, string.Empty, " ", "no@specialcharactersallowed", "nonumb3rsallowed" },
    { o => o.Registration, null, string.Empty, " ", "RN-33-1", "R1-33-X", "1R-33-X", "RR-3A-X" },
    { o => o.FoundedAtDate, default, new DateTime(1899, 12, 31), new DateTime(2000, 1, 1) }
}.ToValidationTestCaseData();

All the tests for a single property can now be as short as a single line. Adding tests for another property now means adding a single line to the collection initialization. Adding and removing invalid sample values is also trivial.

For checking valid cases, a similar method can by applied.

Since the test methods themselves will look almost identical in every validator test class, their code could even be moved into static helpers or a base class for validator tests.

In conclusion, with a couple of lines of code (MemberMultiSamples and the extension method for NUnit) we enabled a pattern of writing a single parameterized test to cover multiple validated properties. This leaves you with just two test methods per validated class: one for valid values, one for invalid values.

You can find the complete example and more included in my TestSamples repo on github