Convenient member data sources with xUnit

In xUnit test cases can be parameterized using the attribute [InlineData] much like [Datarow] in MsTest. That is, instead of stating a [Fact], you can create a [Theory] and parameterize it, much like this example:

public int Add(int x, int y) => x + y;

[Theory]
[InlineData(1,1,2)]
[InlineData(0,1,1)]
[InlineData(-1,1,0)]
[InlineData(1,0,1)]
public void TestAdd(int x, int y, int expected)
{
    int result = Add(x, y);
    Assert.Equal(expected, result);
}

Here we created four testcases using a general parameterized theory. This allows you to rapidly gain coverage without the boilerplate of creating a lot of test methods; with tempting copy/pasting.

However values must be constants, which limits your flexibility using this approach.

Alternatively, using [MemberData] (and [ClassData]) you can create complex data sources for your theories; something like this:

public static IEnumerable<object[]> TestAddDataSource => new List<object[]>()
{
    new object[] {1, 2, 3},
    new object[] {...}
};

We would, however, like to avoid the object-array initialization new object[] {...; and we can actually get away with this, using this simple method:

static object[] Row(params object[] os) => os;

But it actually adds a bit more flexibility, say for instance, allowing for type-conversions where needed.

In a recent project in a test for a code analysis tool, in this example, doing signs-analysis, we did a test for for example the minus operator. In the analysis, values are Integer-values, Top (too much information), or Bottom (no information at all) about a value. Basically, we have concrete information about a value or abstract information about a value. A test-theory for the full set of values would either test concrete values or that the abstractions are the same. This could could look like this:

[Theory]
[MemberData(nameof(TestMinusData))]
public void TestDsMinus(Value x, Value y, Value expected)
{
    Value result = DsMinus(x, y);

    if (expected is Integer exi && result is Integer resultInt)
    {
        Assert.Equal(exi.Value, resultInt.Value);
    } else
        Assert.Equal(expected.GetType(), res.GetType());

}

The TestMinusData would, however, have to look like this:

public static IEnumerable<object[]> TestMinusData => new List<object[]>()
{
    Row(new Integer(0), new Integer(1), new Integer(-1)),
    Row(Value.Bottom,   Value.Bottom,   Value.Bottom),
    Row(new Integer(1), new Integer(1), Value.Top)
}

Although somewhat better, this is still hard to read. With a small change to row, we can make our TestMinusData-method easier to write and to read:

public static IEnumerable<object[]> TestMinusData => new List<object[]>()
{
    Row(1,1,Value.Top),
    Row(0,0,0),
    Row(1,0,1),
    Row(0,1,-1),
    Row(Value.Bottom, 1, Value.Bottom),
    Row(1, Value.Bottom, Value.Bottom),
    Row(Value.Bottom, Value.Bottom, Value.Bottom)
};

Value.Bottom and Value.Top are actually instances of class Bottom and Top respectively, both subclasses of Value. But even if ints were implicitly converted to the Integer-class, it wouldn’t do so automatically; in the actual example, Value, is an interface. We would have to cast or wrap each int, introducing a lot of unwanted noise. Instead, the Row could be altered as below, or specific row methods could be added:

static object[] Row(params object[] os) => 
  os.Select(v =>(v is int i) ? new Integer(i) : v)
  .ToArray();

In the above we convert wrap all ints in Integer, and leave everything else as is. With relatively small changes, we now write

Row(0,1,-1)

instead of

new object[]{ new Integer(0), new Integer(1), new Integer(-1)}

Tests should be simple to read, write and understand. A technique like Row might someday make your life easier.

Using TheoryData is another great way to make good tests in XUnit. Read about it here.

Leave a Comment