Supercharge Your Test Coverage with NUnit’s TestCase Attribute
Save Time and Money Writing Exhaustive C# Tests
Introduction
In this article, I explain the benefits of using NUnit’s TestCaseAttribute, which helps eliminate repetition and make C# tests more expressive.
The Problem
To grasp the reason for using the TestCaseAttibute, it is necessary first to identify the problem it seeks to solve. Let’s write some edge case tests for Math.Pow(double, double), which takes in:
- a base, x
- an exponent, y
and returns the base taken to the power of the exponent:
[TestFixture]
public class Tests
{
[Test]
public void
Pow_X_Zero_Y_More_Than_Zero_Returns_Zero()
{
Assert.AreEqual(0,
Math.Pow(0, 0.1));
} [Test]
public void
Pow_X_Zero_Y_Less_Than_Zero_Returns_PositiveInf()
{
Assert.AreEqual(double.PositiveInfinity,
Math.Pow(0, -0.1));
} [Test]
public void
Pow_X_PositiveInf_Y_More_Than_Zero_Returns_PositiveInf()
{
Assert.AreEqual(double.PositiveInfinity,
Math.Pow(double.PositiveInfinity, 0.1));
} [Test]
public void
Pow_X_PositiveInf_Y_Less_Than_Zero_Returns_Zero()
{
Assert.AreEqual(0,
Math.Pow(double.PositiveInfinity, -0.1));
} [...]}
You’re probably finding yourself doing a lot of copying and pasting, changing the input variables, the expected result, and the test names to reflect different combinations. And, we haven’t finished yet — there are still a lot more tests to write.
Not only does this take longer, but it increases the chances of human error creeping in, requiring more effort for both developers writing the test cases and PR reviewers reading and verifying their correctness.
The Solution
The solution is to use the NUnit TestCaseAttribute, which:
- marks a method with parameters as a test method
- provides data used when calling this test method
- allows us to test a large number of combinations that might not be feasible to check for under time and budget constraints
Instead of writing a new test case for every condition, we instead decorate a single unit test method with the inputs and output, which performs the same test over four separate data sets.
First, write a single unit test method that takes in two parameters — x and y — and asserts against a third input called expectedResult.
[Test]
public void Pow_X_Y_Returns_X_To_Power_Of_Y
(double x, double y, double expectedResult)
{
Assert.AreEqual(expectedResult, Math.Pow(x, y));
}
Second, stack [TestCase] attributes on the test, providing inline data with the following syntax that matches the signature of the unit test method:
[TestCase(0, 0.1, 0)]
[TestCase(0, -0.1, double.PositiveInfinity)]
[TestCase(double.PositiveInfinity, 0.1,
double.PositiveInfinity)]
[TestCase(double.PositiveInfinity, -0.1, 0)]
public void Pow_X_Y_Returns_X_To_Power_Of_Y(
double x,
double y,
double expectedResult)
{
Assert.AreEqual(expectedResult, Math.Pow(x, y));
}
Although we’ve only defined one unit test method, our test results show that four test cases have been passed:
Another variety of the test case attribute approach is to specify a named parameter called ExpectedResult, which means two things for our unit test method. We need to:
- Change the output type from void to double, returning the result instead of using Assert.
- Remove the last parameter in the method signature
In the following example, ExpectedResult is set in each TestCase attribute, replacing the expectedResult parameter in the method signature. I’m also expecting a return value of the type of double:
[TestCase(0, 0.1,
ExpectedResult = 0)]
[TestCase(0, -0.1,
ExpectedResult = double.PositiveInfinity)]
[TestCase(double.PositiveInfinity, 0.1,
ExpectedResult = double.PositiveInfinity)]
[TestCase(double.PositiveInfinity, -0.1,
ExpectedResult = 0)]
public double Pow_X_Y_Returns_X_To_Power_Of_Y(
double x, double y)
{
return Math.Pow(x, y);
}
Again, four test cases pass:
Finally, use the TestName named parameter to provide PR reviewers and future developers with some context about what the test does:
[TestCase(0, 0.1,
ExpectedResult = 0,
TestName = "0^0.1 = 0")]
[TestCase(0, -0.1,
ExpectedResult = double.PositiveInfinity,
TestName = "0^-0.1 = Inf")]
[TestCase(double.PositiveInfinity, 0.1,
ExpectedResult = double.PositiveInfinity,
TestName = "Inf^0.1 = Inf")]
[TestCase(double.PositiveInfinity, -0.1,
ExpectedResult = 0
TestName = "Inf^-0.1 = Inf")]
public double Pow_X_Y_Returns_X_To_Power_Of_Y(
double x, double y)
{
return Math.Pow(x, y);
}
Once more, confirm the test cases pass:
Thanks for reading! Let me know what you think in the comments section below, and don’t forget to subscribe. 👍