xUnit and multiple data records for a test
Asked Answered
M

3

18

I'm fairly new to Unit Testing and have the following code:

public class PowerOf
{
    public int CalcPowerOf(int @base, int exponent) {
        if (@base == 0) { return 0; }
        if (exponent == 0) { return 1; }
        return @base * CalcPowerOf(@base, exponent - 1);
    }
}

The unit test (with xUnit) I wrote for it first was this one, but I'm not quite sure if it's the right approach, or if I should use another pattern? What I wanted to know is whether this is the correct usage for passing multiple sets of data into a "unit test" - as I didn't see any docs or reference examples on xUnit's docs?

    [Fact]
    public void PowerOfTest() {
        foreach(var td in PowerOfTestData()) {
           Assert.Equal(expected, CalcPowerOf(@base, exponent));
        }
    }

    public class TestData {
      int Base {get;set;}
      int Exponent {get;set;}
      int ExpectedResult {get;set;}
    }

    public List<TestData> PowerOfTestData() {
        yield return new TestData { Base = 0, Exponent = 0, TestData = 0 };
        yield return new TestData { Base = 0, Exponent = 1, TestData = 0 };
        yield return new TestData { Base = 2, Exponent = 0, TestData = 1 };
        yield return new TestData { Base = 2, Exponent = 1, TestData = 2 };
        yield return new TestData { Base = 5, Exponent = 2, TestData = 25 };
    }
Matrilineal answered 26/2, 2017 at 10:18 Comment(0)
H
23

You'd be better of using a specialised construct in xUnit, called a Theory, that handles so called "Data Driven Tests". Decorate your testmethod with the Theory attribute and then make sure to return a static "member" with input parameters and the expected result as you already kind of did with the TestData class. See the example below, and ref to the xUnit documentation: "Writing your first theory".

I would thus refactor your code like below. Firstly decorating the test with the attributes Theory and MemberData and adding parameters to your test "@base", "exponent" and "expectedResult" - as you had in your TestData class. xUnit won't allow you to use the TestData class, it only accepts an IEnumerable<object> and requires it to be static, but the benefit to a foreach loop construct is that all the tests are ran seperately. And for each run with a specific data set you'll get a green or red flag!

public class PowerOfTests
{
    [Theory]
    [MemberData(nameof(PowerOfTestData))]
    public void PowerOfTest(int @base, int exponent, int expected) {
        Assert.Equal(expected, CalcPowerOf(@base, exponent));
    }

    public static IEnumerable<object[]> PowerOfTestData() {
        yield return new object[] { 0, 0, 0 };
        yield return new object[] { 0, 1, 0 };
        yield return new object[] { 2, 0, 1 };
        yield return new object[] { 2, 1, 2 };
        yield return new object[] { 2, 2, 4 };
        yield return new object[] { 5, 2, 25 };
        yield return new object[] { 5, 3, 125 };
        yield return new object[] { 5, 4, 625 };
    }
}
Hendrik answered 26/2, 2017 at 10:18 Comment(5)
Can I never use a special class?Matrilineal
You can, but you will always have to return an IEnumerable of object[] - as that is what's referred to in the xUnit documentation.Hendrik
Remark for everyone in 2022 onwards: Use TheoryData github.com/xunit/xunit/blob/v2/src/xunit.core/TheoryData.cs it has some nice integrationTopographer
@Topographer - feel free to edit that in the question!Hendrik
@YvesSchelpe do you mean in your answer? If so - feel free to mark it as a community answer if you want!Topographer
A
19

You're using a class member to define your data which is wrong in your case. You use this approach when the values are specified at run-time (maybe looping through values from 1 to MAX) which is not your case (You have hard-coded data). I think this approach is better:

[Theory]
[InlineData(0, 0, 0)]
[InlineData(0, 1, 0)]
[InlineData(2, 0, 1)]
[InlineData(2, 1, 2)]
[InlineData(2, 2, 4)]
[InlineData(5, 2, 25)]
[InlineData(5, 3, 125)]
[InlineData(5, 4, 625)]
public void PowerOfTest(int @base, int exponent, int expected) 
{
   var result = CalcPowerOf(@base,exponent);
   Assert.Equal(expected, result);
}

This way you have a more readable test in a large class.

Aperient answered 26/2, 2017 at 10:48 Comment(1)
Thank you. Marked the other one as answer because 1st - and I prefer the less annotated version.Matrilineal
C
5

For a strongly-typed parameter list to the test method without using object[], you can also use TheoryData. It defines several generic overloads for up to 10 parameters. Since your method has 3 integer input values, @base, exponent and expected, you can use a property of type TheoryData<int, int, int>. Then, annotate your PowerOfTest method with the Theory and MemberData(nameof(PropertyName) attributes:

class PowerOfTests
{
    public static TheoryData<int, int, int> PowerOfTestData => new ()
    {
       { 0, 0, 0 },
       { 0, 1, 0 },
       { 2, 0, 1 },
       { 2, 1, 2 },
       { 5, 2, 25 }
    };
     
    [Theory]
    [MemberData(nameof(PowerOfTestData))] 
    public void PowerOfTest(int @base, int exponent, int expected) 
    {
        Assert.Equal(expected, CalcPowerOf(@base, exponent));
    }
}

The reason I can initialize TheoryData<int, int, int> with the:

{ 
    { param1, param2, param3 }, 
    ... 
}

syntax(called a collection initializer) is because it implements IEnumerable and defines an Add<int, int, int>(int, int, int) method that takes in three integer parameters( the <int, int, int> generic overload of TheoryData).

This also makes it possible to have the test data in a separate class by inheriting from TheoryData:

class PowerOfTestDataClass : TheoryData<int, int, int>
{
    public PowerOfTestDataClass()
    {
       Add(0, 0, 0);
       Add(0, 1, 0);
       Add(2, 0, 1);
       Add(2, 1, 2);
       Add(5, 2, 25);
    }
}

Now instead of MemberData, annotate the PowerOfTest() method with the ClassData attribute and its parameter as PowerOfTestDataClass's type:

[Theory]
[ClassData(typeof(PowerOfTestDataClass)] 
public void PowerOfTest(int @base, int exponent, int expected) 
{
    Assert.Equal(expected, CalcPowerOf(@base, exponent));
}

The advantage of having a strongly-typed typed parameter list is you can always ensure that the arguments will have the right type and the right length. While the object array in IEnumerable<object[]> also works, it will allow any type and any length.

Reference: https://andrewlock.net/creating-strongly-typed-xunit-theory-test-data-with-theorydata/

Cerecloth answered 9/7, 2021 at 18:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.