Power Up Your Tests: An Intro to Data-Driven Testing
You've successfully set up NUnit and written your first tests. That's fantastic! Now, let's unlock a feature that will dramatically increase your efficiency and test coverage: Data-Driven Testing.
Imagine you need to test our PoliteCalculator.Add() method. You'd want to check it with positive numbers, negative numbers, and with zero. Would you write three almost identical test methods for this? Absolutely not! That would be a maintenance nightmare.
Data-driven testing is the elegant solution. It allows us to write a single test method and execute it multiple times with different sets of input data and expected results. Let's power up your tests! 🔋
The Problem with Repetitive Tests
Let's look at the "brute force" way of testing our calculator's Add method for different scenarios. We would end up with code that looks like this:
[TestFixture]
public class RepetitiveCalculatorTests
{
[Test]
public void Add_TwoPositiveNumbers_ShouldReturnCorrectSum()
{
var calc = new PoliteCalculator();
Assert.That(4, Is.EqualTo(calc.Add(2, 2)));
}
[Test]
public void Add_TwoNegativeNumbers_ShouldReturnCorrectSum()
{
var calc = new PoliteCalculator();
Assert.That(-10, Is.EqualTo(calc.Add(-5, -5)));
}
[Test]
public void Add_PositiveAndZero_ShouldReturnCorrectSum()
{
var calc = new PoliteCalculator();
Assert.That(10, Is.EqualTo(calc.Add(10, 0)));
}
}
While this works, it's very inefficient. The test logic is identical in all three methods; only the input values and the expected result change. If we needed to change the way we call the Add method, we'd have to update all three tests. This violates the DRY (Don't Repeat Yourself) principle and doesn't scale well.
We need a way to separate the test logic from the test data.
Enhancing Test Efficiency with [TestCase]
NUnit provides a simple and powerful attribute for basic data-driven testing: [TestCase]. This attribute allows you to run a single test method multiple times with different arguments.
Here's how it works:
- You add parameters to your test method signature to accept the input data and the expected result.
- You remove the
[Test]attribute. - You add one
[TestCase(...)]attribute above your method for each set of data you want to test. The values inside the parentheses are passed directly to your method's parameters in order.
Let's refactor our repetitive calculator tests into a single, elegant data-driven test:
[TestFixture]
public class DataDrivenCalculatorTests
{
// This single method will be run three times, once for each TestCase.
[TestCase(2, 2, 4)]
[TestCase(-5, -5, -10)]
[TestCase(10, 0, 10)]
[TestCase(-5, 10, 5)]
public void Add_VariousNumbers_ShouldReturnCorrectSum(int num1, int num2, int expectedResult)
{
var calc = new PoliteCalculator();
int actualResult = calc.Add(num1, num2);
Assert.That(expectedResult, Is.EqualTo(actualResult));
}
}
Look how much cleaner that is! We now have one test method that handles four different scenarios. If we need to change the test logic, we only have to do it in one place. When you run this in the Test Explorer, you will see four separate test results, one for each [TestCase].
Making Tests More Readable
The [TestCase] attribute has some handy named properties that can make your test results even clearer.
Using TestName
By default, the Test Explorer might name your tests something like Add_VariousNumbers_ShouldReturnCorrectSum(2, 2, 4). This is okay, but we can make it more descriptive using the TestName property.
Using ExpectedResult
Even better, you can use the ExpectedResult property to streamline your assertions. This tells NUnit what the method is expected to return, and NUnit will automatically perform the Assert.That for you! This makes your test body even leaner.
Let's refactor our test to use these features:
[TestFixture]
public class ReadableDataDrivenTests
{
// The test method now returns the value it wants to assert.
[TestCase(2, 2, ExpectedResult = 4, TestName = "Add_TwoPositiveNumbers")]
[TestCase(-5, -5, ExpectedResult = -10, TestName = "Add_TwoNegativeNumbers")]
[TestCase(10, 0, ExpectedResult = 10, TestName = "Add_PositiveAndZero")]
[TestCase(-5, 10, ExpectedResult = 5, TestName = "Add_NegativeAndPositive")]
public int Add_VariousNumbers_ShouldReturnCorrectSum(int num1, int num2)
{
// Arrange
var calc = new PoliteCalculator();
// Act
int result = calc.Add(num1, num2);
// Assert - is now handled by NUnit via ExpectedResult!
return result;
}
}
Now our test method body contains only Arrange and Act steps, making it incredibly clean. In the Test Explorer, you'll see your custom test names, which is much better for reporting!
Sourcing Data with [TestCaseSource]
The [TestCase] attribute is perfect for simple, static data. But what if your test data is more complex, needs to be generated programmatically, or you want to keep it separate from your test method? For these scenarios, NUnit provides the [TestCaseSource] attribute.
This attribute tells NUnit to get the test data from a static method, field, or property within your class (or another class). This source must return a collection (like an IEnumerable) of test case data.
Let's create a source for our calculator tests:
[TestFixture]
public class SourcedDataTests
{
// 1. Define a static source that provides the test data.
// It returns an array of arrays. Each inner array is one test case.
static object[] AddCases =
{
new object[] { 2, 2, 4 },
new object[] { -5, -5, -10 },
new object[] { 10, 0, 10 }
};
// 2. Use TestCaseSource to point to our data source by its name.
[TestCaseSource(nameof(AddCases))]
public void Add_Tests_FromSource(int num1, int num2, int expectedResult)
{
var calc = new PoliteCalculator();
int actualResult = calc.Add(num1, num2);
Assert.That(expectedResult, Is.EqualTo(actualResult));
}
}
The nameof(AddCases) syntax is a safe way to refer to the AddCases member, preventing errors if you rename it later.
Keeping Data Separate
The real power of [TestCaseSource] is that your data source could be a method that reads test data from a file (like a CSV or JSON file) or even a database. This allows you to completely separate your test logic from your test data, which is a key principle of robust test automation framework design.
This keeps your tests clean and allows non-programmers to potentially add new test scenarios just by editing a data file.
When to Use Data-Driven Testing
Data-driven testing is one of the most powerful concepts you can apply to make your test suite more effective and efficient. It's the perfect strategy whenever you have a single workflow or function that you need to verify with multiple different inputs and expected outputs.
Prime use cases in test automation include:
- Testing a login form: You can have test cases for a valid user, a user with the wrong password, a locked-out user, a user with an invalid email format, etc., all running through the same test method.
- Verifying calculations: Just like our calculator example, any feature that performs calculations should be tested with a range of values, including positive, negative, zero, and boundary values.
- Form submissions: Testing a complex registration or data entry form with various combinations of valid data, invalid data, required fields left blank, and optional fields filled in.
- API endpoint testing: Sending different request bodies to an API endpoint and asserting that you get the correct status codes and response bodies for each case.
By using a data-driven approach, you drastically reduce code duplication and can easily expand your test coverage by simply adding another [TestCase] or another entry to your data source.
Key Takeaways
- Data-Driven Testing is the practice of running a single test method multiple times with different sets of input data and expected outcomes.
- It dramatically reduces code duplication and makes it easy to increase test coverage for a specific feature.
- NUnit's
[TestCase]attribute is a simple and powerful way to supply static data directly to a test method. - You can make test results more readable by using the
TestNameandExpectedResultproperties of the[TestCase]attribute. - For more complex or externally-stored data, the
[TestCaseSource]attribute allows you to pull test data from a static method, field, or property.