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 };
}
xUnit.net creates a new instance of the test class for every test that is run, so any code which is placed into the constructor of the test class will be run for every single test.
As far as NUnit vs. XUnit vs. MSTest is concerned, the biggest difference between xUnit and the other two test frameworks (NUnit and MSTest) is that xUnit is much more extensible when compared to NUnit and MSTest. The [Fact] attribute is used instead of the [Test] attribute.
Fact vs Theory In an Xunit test class or fixture, there are two kinds of tests: Fact tests and Theory tests. The small, but very important, difference is that Theory tests are parameterized and can take outside input. Fact tests, however, are not parameterized and cannot take outside input.
Running unit tests in parallel is a new feature in xUnit.net version 2. There are two essential motivations that drove us to not only enable parallelization, but also for it to be a feature that's enabled by default: As unit testing has become more prevalent, so too have the number of unit tests.
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.
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)
attribute:
class PowerOfTests
{
TheoryData<int, int, int> PowerOfTestData => new TheoryData<int, int, int>
{
{ 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/
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 };
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With