1
Write a post

Intro to Unit Testing C# code with NUnit and Moq (Part 2)

Published Jun 07, 2017Last updated Jun 09, 2017
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:

Quick and Easy Data-Driven Tests With TestCaseAttribute

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.

Handle More Complex Scenarios with TestCaseSourceAttribute

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 and static
  • 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!

Discover and read more posts from Ed Mays
get started
Enjoy this post?

Leave a like and comment for Ed

2
Be the first to share your opinion

Get curated posts in your inbox

Learn programming by reading more posts like this