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.