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:

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:

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:

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:

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:

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:

publicclassCalculatorTestData:TheoryData<int,int,int>{publicCalculatorTestData(){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:

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:

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.