Intro to Unit Testing C# code with NUnit and Moq (Part 2)
Data-Driven Testing with NUnit
In Part 1 of this series, we took a quick look at two common, mature tools for unit testing C# code - NUnit and Moq. We also covered some basic unit testing philosophies and ways to improve your code's testability.
As you adopt a TDD strategy, you will find situations where it's beneficial to have data-driven tests - tests that execute multiple times with varying inputs and expected results. We touched on this concept briefly in Part 1 - in fact, let's start with a quick refresher:
TestCaseAttribute
Quick and Easy Data-Driven Tests With TestCaseAttribute is an Attribute we can attach to any NUnit TestMethod, to provide arguments to the test at runtime, for example:
[TestCase("Hello", false, "Hello was False")]
public void SomeMethod_Always_DoesSomethingWithParameters(string input1, bool input2, string expectedResult){
// the actual test implementation isn't important, just know that when this test runs:
// input1 == "Hello"
// input2 == "False"
// expectedResult == "Hello was False"
}
We can also attach multiple TestCases to a single TestMethod. When we do this, the test method runs once for each TestCaseAttribute:
[TestCase("Hello", false, "Hello was False")]
[TestCase("Goodbye", true, "Goodbye was True")]
public void SomeMethod_Always_DoesSomethingWithParameters(string input1, bool input2, string expectedResult){
// This test will run twice, once for each TestCaseAttribute.
}
Note that when using
TestCaseAttribute
, the number of arguments provided to the attribute must match the number, order, and type of arguments expected by the test.
TestCaseAttribute is generally sufficient for basic parameterized tests, however this approach does have certain limitations, for example if your test depends on a value that is not known at runtime:
// ** WILL NOT COMPILE - DateTime.Today() is not a compile-time constant, so cannot be used
// as an Attribute value.
[TestCase("Hello", DateTime.Today(), "Hello was False")]
public void SomeMethod_Always_DoesSomethingWithParameters(string input1, DateTime input2, string expectedResult){
}
Another limitation of TestCaseAttribute
is that it is not possible to reuse your test case data because the attribute containing the test data is tied to a single TestMethod.
TestCaseSourceAttribute
Handle More Complex Scenarios with For more advanced scenarios, including values determined at runtime, NUnit provides TestCaseSourceAttribute
and a related class, TestCaseData
. This attribute differs from TestCaseAttribute
in that instead of directly specifying values to inject, TestCaseSourceAttribute
uses an indirect approach via specialized static properties:
[TestFixture]
public class TestClass{
public static IEnumerable<TestCaseData> SomeTestCases{
get{
yield return new TestCaseData("Hello", DateTime.Today(), "Hello was True");
yield return new TestCaseData("Goodbye", DateTime.Now(), "Hello was False");
}
}
// Notice that the
[TestCaseSource(typeof(TestClass), "SomeTestCases")]
public void SomeMethod_Always_DoesSomethingWithParameters(string input1, DateTime input2, string expectedResult){
// This test will run once for each yield return statement in SomeTestCases
}
// ... Rest of class omitted ...
}
So let's go through this code to get a grasp on the moving parts. The test is very similar to the prior examples, but instead of being decorated with TestCaseAttribute
, it is decorated with TestCaseSourceAttribute
. The attribute's signature is also different - When using TestCaseSourceAttribute
, we must specify the type that contains the data (in this example, the test class contains/exposes the test data but that's not a requirement - more on that below...), along with the name of a property from which to fetch the test cases.
The property exposing the test data also has some constraints that must be followed:
- The property must be
public
andstatic
- It must be of type
IEnumerable<TestCaseData>
- It does not need a setter
Within the test case property getter, we use a standard yield return
pattern to return successive sets of TestCaseData.
If the
yield return
pattern is unfamiliar to you, think of it as a way to iterate through a list item by item until no items remain. In the code above, the first time the SomeTestCases getter is invoked (by NUnit), the first TestCaseData is returned. The next time it is invoked, the next TestCaseData is returned, and so forth.
It's also important to note that the TestCaseData
properties must match up with the test method's signature, just like TestCaseAttribute
Separate Tests from Test Data to Keep Your Test Suites Clean
Another advantage of the TestCaseSource approach is that it enables us to cleanly separate our test data from our actual tests. For example, imagine if the TestClass above had 10 TestMethods with different TestCaseSources for each - the class would be fairly large and complex, mixing data with test functionality. By separating our test data from our tests, we make our test fixtures much more maintainable.
Let's refactor the previous example to separate data from tests:
[TestFixture]
public class TestClass{
[TestCaseSource(typeof(TestClassData), "SomeTestCases")]
public void SomeMethod_Always_DoesSomethingWithParameters(string input1, DateTime input2, string expectedResult){
// This test will run once for each yield return statement in SomeTestCases
}
// ... Rest of class omitted ...
}
public class TestClassData{
public static IEnumerable<TestCaseData> SomeTestCases{
get{
yield return new TestCaseData("Hello", DateTime.Today(), "Hello was True");
yield return new TestCaseData("Goodbye", DateTime.Now(), "Hello was False");
}
}
}
The code is nearly identical, except that the IEnumerable<TestCaseData> property has been extracted out into its own class, leaving TestClass to implement only test-related functionality. Additionally, under certain circumstances we may be able to reuse data sources across test suites, reducing code duplication and test implementation effort.
As a quick exercise, let's refactor the CreditDecisionTests from Part 1 of this series to use TestCaseSource
instead of TestCase
:
Original Code:
[TestCase(100, "Declined")]
[TestCase(549, "Declined")]
[TestCase(550, "Maybe")]
[TestCase(674, "Maybe")]
[TestCase(675, "We look forward to doing business with you!")]
public void MakeCreditDecision_Always_ReturnsExpectedResult(int creditScore, string expectedResult){
var result = systemUnderTest.MakeCreditDecision(creditScore);
Assert.That(result, Is.EqualTo(expectedResult);
}
We can easily refactor these 5 test cases into a TestCaseSource, as below:
Refactored to use TestCaseSource:
[TestFixture]
public class CreditDecisionTests{
public static IEnumerable<TestCaseData> CreditDecisionTestData{
get{
yield return new TestCaseData(100, "Declined");
yield return new TestCaseData(549, "Declined");
yield return new TestCaseData(550, "Maybe");
yield return new TestCaseData(674, "Maybe");
yield return new TestCaseData(675, "We look forward to doing business with you!");
}
}
[TestCaseSource(typeof(CreditDecisionTests), "CreditDecisionTestData")]
public void MakeCreditDecision_Always_ReturnsExpectedResult(int creditScore, string expectedResult){
var result = systemUnderTest.MakeCreditDecision(creditScore);
Assert.That(result, Is.EqualTo(expectedResult);
}
}
The end result is functionally equivalent, but now the test data is separated out from the test logic, so the code is cleaner and easier to maintain.
Conclusion
Today we explored two ways to implement parameterized, data-driven tests using NUnit. Data-driven testing is useful when testing multiple execution paths, edge cases, and other scenarios where expectations may vary based on inputs. When implemented correctly, data-driven testing also enables us to reuse test case data for multiple test cases, reducing code duplication and test development time.
I hope this short tutorial has provided you with additional understanding of NUnit's powerful data-driven testing features and how they can be leveraged in a TDD environment.
Stay tuned for future tutorials, where we will cover advanced Moq scenarios including sequences and event testing!
Looking forward to the next part!
Really nice introduction to NUnit. Why didn’t Google find this for me this time last year?! Part one introduced Moq but part two did not extend on the use of Moq. Have you any more Moq examples?
Thank you for these amazing posts. Can’t wait to see the third part! May I ask, when it will arrive?