Data-driven testing is a testing method where test data is provided through some external source. Hence it's also known as parameterized testing.
A popular testing library in .NET that supports parameterized testing is xUnit. It uses attributes to define test methods. The Fact
attribute defines a simple test, and the Theory
attribute defines a parameterized test.
In this week's newsletter, I'm going to show you four ways to write parameterized tests with xUnit:
InlineData
MemberData
ClassData
TheoryData
And I'll discuss which approach I think is the best.
Writing Parameterized Tests With InlineData
The simplest way to write parameterized tests with xUnit is using the InlineData
attribute. You provide test data by passing in values to the InlineData
constructor.
Here's how that would look like:
[Theory]
[InlineData("test@test.com", "test.com")]
[InlineData("milan@milanjovanovic.tech", "milanjovanovic.tech")]
public void EmailParser_Should_Return_Domain(string email, string expectedDomain)
{
// Arrange
var parser = new EmailParser();
// Act
var domain = parser.ParseDomain(email);
// Assert
Assert.Equal(domain, expectedDomain);
}
In this example, we provide two string
values to the InlineData
attribute, which represent the email
and expectedDomain
parameters in the test. We can specify the InlineData
attribute as many times as we want, to introduce more test cases.
The downside of this approach is that it becomes very verbose when we have many test cases. And we are limited to only using constant data for the parameters.
Writing Parameterized Tests With MemberData
With the MemberData
attribute we have the ability to programmatically provide the test data. You can load the test data from a static property or member of a type.
Here's an example of using the MemberData
attribute to load test data from a property:
[Theory]
[MemberData(nameof(EmailTestData))]
public void EmailParser_Should_Return_Domain(string email, string expectedDomain)
{
// Arrange
var parser = new EmailParser();
// Act
var domain = parser.ParseDomain(email);
// Assert
Assert.Equal(domain, expectedDomain);
}
public static IEnumerable<object[]> EmailTestData => new List<object>
{
new object[] { "test@test.com", "test.com" },
new object[] { "milan@milanjovanovic.tech", "milanjovanovic.tech" }
};
You specify the name of the member in the MemberData
attribute, and it's a best practice to use the nameof
operator so that you can rename the property (or method) in the future without breaking your test.
The one constraint is that the property (or method) has to return IEnumerable<object[]>
, so there is no strong typing.
Writing Parameterized Tests With ClassData
The ClassData
attribute allows you to extract test data into its own class. This is helpful for organizing your test data separately from your tests, and it allows for easier reuse. You load the test from a class the inherits from IEnumerable<object[]>
and implements the GetEnumerator
method.
Here's an example of using the ClassData
attribute to load test data from a class:
[Theory]
[ClassData(typeof(EmailTestData))]
public void EmailParser_Should_Return_Domain(string email, string expectedDomain)
{
// Arrange
var parser = new EmailParser();
// Act
var domain = parser.ParseDomain(email);
// Assert
Assert.Equal(domain, expectedDomain);
}
public class EmailTestData : IEnumerable<object[]>
{
public IEnumerable<object[]> GetEnumerator()
{
yield return new object[] { "test@test.com", "test.com" };
yield return new object[] { "milan@milanjovanovic.tech", "milanjovanovic.tech" };
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
};
Unfortunately, this approach is complicated because you have to implement the IEnumerable
interface.
It almost defeats the purpose of separating test data from the actual tests.
And we still suffer from lack of type-safety.
Is there a better solution?
Writing Parameterized Tests With TheoryData
Let me introduce you to TheoryData
, which is my preferred way of providing test data for parameterized tests. Using the TheoryData
class you can implement a class to provide test data while having the benefit of type-safety.
Here's an example of using TheoryData
in combination with ClassData
:
[Theory]
[ClassData(typeof(EmailTestData))]
public void EmailParser_Should_Return_Domain(string email, string expectedDomain)
{
// Arrange
var parser = new EmailParser();
// Act
var domain = parser.ParseDomain(email);
// Assert
Assert.Equal(domain, expectedDomain);
}
public class EmailTestData : TheoryData<string, string>
{
public EmailTestData()
{
Add("test@test.com", "test.com");
Add("milan@milanjovanovic.tech", "milanjovanovic.tech");
}
};
How does TheoryData
work?
It's a generic class that allows us to specify the types for our parameterized test.
You just call the Add
method in the EmailTestData
constructor to provide test data for a single test case. And introducing more test cases comes down to calling the Add
method multiple times.
You can also use TheoryData
in combination with MemberData
, and return TheoryData
from a property or method.
Which Approach Should You Use?
I showed you four approaches to write parameterized tests with xUnit:
InlineData
MemberData
ClassData
TheoryData
So which one should you use?
Here's my personal preference that you can follow if you want:
-
InlineData
for simple test cases -
TheoryData
usingClassData
for complex test cases
Thank you for reading, and have a wonderful Saturday.
P.S. Whenever you're ready, there are 2 ways I can help you:
Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 950+ students here.
Patreon Community: Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses. Join 820+ engineers here.