Thank you to our sponsors who keep this newsletter free to the reader:
This week's issue is sponsored by Localizely. Localizely is the translation management platform that helps you translate texts in your app for targeting multilingual markets. With Localizely you can collaborate with your team on translations, and reduce translation errors with better context and app screenshots. Learn more here.
And by QA Wolf. QA Wolf gets you to 80% automated end-to-end test coverage in 4 months - and keeps you there. They will build, run and maintain your test suite so that you don't have to. Chat with QA Wolf.
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.
Let's dive in.
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 casesTheoryData
usingClassData
for complex test cases
Thank you for reading, and have a wonderful Saturday.