blog post image
Andrew Lock avatar

Andrew Lock

~4 min read

Creating strongly typed xUnit theory test data with TheoryData

In a recent post I described the various ways you can pass data to xUnit theory tests using attributes such as [InlineData], [ClassData], or [MemberData]. For the latter two, you create a property, method or class that returns IEnumerable<object[]>, where each object[] item contains the arguments for your theory test.

In this post, I'll show an alternative way to pass data to your theory tests by using the strongly-typed TheoryData<> class. You can use it to create test data in the same way as the previous post, but you get the advantage of compile-time type checking (as you should in C#!)

The problem with IEnumerable<object[]>

I'll assume you've already seen the previous post on how to use [ClassData] and [MemberData] attributes but just for context, this is what a typical theory test and data function might look like:

public class CalculatorTests  
{
    [Theory]
    [MemberData(nameof(Data))]
    public void CanAdd(int value1, int value2, int expected)
    {
        var calculator = new Calculator();
        var result = calculator.Add(value1, value2);
        Assert.Equal(expected, result);
    }

    public static IEnumerable<object[]> Data =>
        new List<object[]>
        {
            new object[] { 1, 2, 3 },
            new object[] { -4, -6, -10 },
            new object[] { -2, 2, 0 },
            new object[] { int.MinValue, -1, int.MaxValue },
        };
}

The test function CanAdd(value1, value2, expected) has three int parameters, and is decorated with a [MemberData] attribute that tells xUnit to load the parameters for the theory test from the Data property.

This works perfectly well, but if you're anything like me, returning an object[] just feels wrong. As we're using objects, there's nothing stopping you returning something like this:

public static IEnumerable<object[]> Data =>
    new List<object[]>
    {
        new object[] { 1.5, 2.3m, "The value" }
    };

This compiles without any warnings or errors, even from the xUnit analyzers. The CanAdd function requires three ints, but we're returning a double, a decimal, and a string. When the test executes, you'll get the following error:

Message: System.ArgumentException : Object of type 'System.String' cannot be converted to type 'System.Int32'.

That's not ideal. Luckily, xUnit allows you to provide the same data as a strongly typed object, TheoryData<>.

Strongly typed data with TheoryData

The TheoryData<> types provide a series of abstractions around the IEnumerable<object[]> required by theory tests. It consists of a TheoryData base class, and a number of generic derived classes TheoryData<>. The basic abstraction looks like the following:

public abstract class TheoryData : IEnumerable<object[]>
{
    readonly List<object[]> data = new List<object[]>();

    protected void AddRow(params object[] values)
    {
        data.Add(values);
    }

    public IEnumerator<object[]> GetEnumerator()
    {
        return data.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

This class implements IEnumerable<object[]> but it has no other public members. Instead, the generic derived classes TheoryData<> provide a public Add<T>() method, to ensure you can only add rows of the correct type. For example, the derived class with three generic arguments looks likes the following:

public class TheoryData<T1, T2, T3> : TheoryData
{
    /// <summary>
    /// Adds data to the theory data set.
    /// </summary>
    /// <param name="p1">The first data value.</param>
    /// <param name="p2">The second data value.</param>
    /// <param name="p3">The third data value.</param>
    public void Add(T1 p1, T2 p2, T3 p3)
    {
        AddRow(p1, p2, p3);
    }
}

This type just passes the generic arguments to the protected AddRow() command, but it enforces that the types are correct, as the code won't compile if you try and pass an incorrect parameter to the Add<T>() method.

Using TheoryData with the [ClassData] attribute

First, we'll look at how to use TheoryData<> with the [ClassData] attribute. You can apply the [ClassData] attribute to a theory test, and the referenced type will be used to load the data. In the previous post, the data class implemented IEnumerable<object[]>, but we can alternatively implement TheoryData<T1, T2, T3> to ensure all the types are correct, for example:

public class CalculatorTestData : TheoryData<int, int, int>
{
    public CalculatorTestData()
    {
        Add(1, 2, 3);
        Add(-4, -6, -10);
        Add(-2, 2, 0);
        Add(int.MinValue, -1, int.MaxValue);
        Add(1.5, 2.3m, "The value"); // will not compile!
    }
}

You can apply this to your theory test in exactly the same way as before, but this time you can be sure that every row will have the correct argument types:

[Theory]
[ClassData(typeof(CalculatorTestData))]
public void CanAdd(int value1, int value2, int expected)
{
    var calculator = new Calculator();
    var result = calculator.Add(value1, value2);
    Assert.Equal(expected, result);
}

The main thing to watch out for here is that that the CalculatorTestData implements the correct generic TheoryData<> - there's no compile time checking that you're referencing a TheoryData<int, int, int> instead of a TheoryData<string> for example.

Using TheoryData with the [MemberData] attribute

You can use TheoryData<> with [MemberData] attributes as well as [ClassData] attributes. Instead of referencing a static property that returns an IEnumerable<object[]>, you reference a property or method that returns a TheoryData<> object with the correct parameters.

For example, we can rewrite the Data property from the start of this post to use a TheoryData<int, int, int> object:

public static TheoryData<int, int, int> Data
{
    get
    {
        var data = new TheoryData<int, int, int>();
        data.Add(1, 2, 3);
        data.Add(-4, -6, -10 );
        data.Add( -2, 2, 0 );
        data.Add(int.MinValue, -1, int.MaxValue );
        data.Add( 1.5, 2.3m, "The value"); // won't compile
        return data;
    }
}

This is effectively identical to the original example, but the strongly typed TheoryData<> won't let us add invalid data.

That's pretty much all there is to it, but if the verbosity of that example bugs you, you can make use of collection initialisers and expression bodied members to give:

public static TheoryData<int, int, int> Data =>
    new TheoryData<int, int, int>
        {
            { 1, 2, 3 },
            { -4, -6, -10 },
            { -2, 2, 0 },
            { int.MinValue, -1, int.MaxValue }
        };

As with the [ClassData] attribute, you have to manually ensure that the TheoryData<> generic arguments match the theory test parameters they're used with, but at least you can be sure all of the rows in the IEnumerable<object[]> are consistent!

Summary

In this post I described how to create strongly-typed test data for xUnit theory tests using TheoryData<> classes. By creating instances of this class instead of IEnumerable<object[]> you can be sure that each row of data has the correct types for the theory test.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?