While my previous answer works just fine, I've noticed that it uses an anti-pattern. The IXunitSerializable
interface is part of the xunit.abstractions
NuGet Package, and including a package reference to a unit testing package in the project under test introduces a test concern into the project under test, which doesn't feel like a good practice.
How important the presence of this anti-pattern is, is something for each organisation / development team / solo developer to decide for themselves - some may decide that it doesn't really matter for their use case.
The explanation of why some parameterised test methods can't be expanded to their test cases is still valid, and judging by the upvotes, a few people have already found that answer useful, so I'm leaving it as it is.
Here is an alternative approach that I've been using recently, which bypasses the need for Xunit to be able to serialize all the parameter values, by instead storing all the test cases in a Dictionary<string, TValue>
, where the key is a unique name for the test case. The test case name is then used as the parameter to pass to the test method, and because this name is a string
, it is always serializable, regardless of the values in the test case that the name refers to, and so the tests can always be expanded in the VS test explorer to show the individual test cases.
First we need to define the "shape" of each test case - this encapsulates the values which we'd otherwise be passing as parameters to the test method. For the purpose of this answer, I'll continue to use the slightly contrived example from the question, of testing whether the FromArgb
method of System.Drawing.Color
sets the R
, G
and B
properties correctly.
public record ColorTestCase(Color Color, int Red, int Green, int Blue);
This doesn't actually need to be a record
, you could use a tuple instead if you prefer, or if you're constrained to using an earlier version of C# than 9.0 then a class or struct would work just as well. It also doesn't need to be public
, I've made it public because I'll be using it in multiple classes, but if I were using it only in a single test class then I would nest it within that test class and make it private
.
Next we need to define the data which makes up the test cases. I'll show two different ways of doing this:
[MemberData]
to access data within the same test class[MemberData]
to access data provided by a separate class[MemberData]
to access data within the same test classThis is proably the simplest way of doing it, and is great if your test cases will only be used in a single test class. The property that we name in the test method's [MemberData]
attribute returns the names of all the test cases. The actual test data is a Dictionary
of string
(the test case name) and whatever type you're using as the shape of the test cases. This can be private
because no other classes need to use it, and it needs to be static
so that the static
property that we name in the [MemberData]
attribute can reference it.
The first thing that the test method needs to do is to use the name of the test case to look up the actual test data object from the Dictionary
, and where it would previously use the parameters which make up the test case, it now uses the properties of that test case object.
public class MyTestClass
{
private static Dictionary<string, ColorTestCase> PrimaryColorTestData
= new Dictionary<string, ColorTestCase>()
{
{ "Red", new ColorTestCase(Color.FromArgb(255, 0, 0), 255, 0, 0) },
{ "Green", new ColorTestCase(Color.FromArgb(0, 255, 0), 0, 255, 0) },
{ "Blue", new ColorTestCase(Color.FromArgb(0, 0, 255), 0, 0, 255) }
};
public static TheoryData<string> PrimaryColorTestCaseNames
=> new TheoryData<string>(PrimaryColorTestData.Keys); // Xunit 2.6.5+
[Theory]
[MemberData(nameof(PrimaryColorTestCaseNames))]
public void TestMethodUsingMemberDataAndPrivateDictionary(string testCaseName)
{
var testCase = PrimaryColorTestData[testCaseName];
Assert.Equal(testCase.Red, testCase.Color.R);
Assert.Equal(testCase.Green, testCase.Color.G);
Assert.Equal(testCase.Blue, testCase.Color.B);
}
}
The TheoryData<T>
constructor which accepts a IEnumerable<T>
parameter was added in Xunit 2.6.5, so if you're using an earlier version and aren't able to upgrade then you need to add the names of the test casees in a loop.
public static TheoryData<string> PrimaryColorTestCaseNames
{
get
{
var data = new TheoryData<string>();
foreach (var key in ColorTestDataProvider.BlackAndWhiteTestData.Keys)
{
data.Add(key);
}
return data;
}
}
[MemberData]
to access data provided by a separate classThis approach is useful if your test data is getting large and you want to keep it separate from the test class, or if you want to use the same test data in multiple test classes.
First we need a class which provides our test data and the names of our test cases. This time the test data needs to be a public
property rather than a private
field so that the test class can access it.
public static class ColorTestDataProvider
{
public static Dictionary<string, ColorTestCase> BlackAndWhiteTestData
=> new Dictionary<string, ColorTestCase>()
{
{ "Black", new ColorTestCase(Color.FromArgb(0, 0, 0), 0, 0, 0) },
{ "White", new ColorTestCase(Color.FromArgb(255, 255, 255), 255, 255, 255) },
};
public static TheoryData<string> BlackAndWhiteTestCaseNames
=> new TheoryData<string>(BlackAndWhiteTestData.Keys);
}
Next, we need to add a test method to MyTestClass
with a [MemberData]
attribute which specifies the name and declaring type of the property which provides the names of the test cases. Again, the first thing the test method needs to do is to use the name of the test case to look up the test data object in the Dictionary
.
[Theory]
[MemberData(
nameof(ColorTestDataProvider.BlackAndWhiteTestCaseNames),
MemberType = typeof(ColorTestDataProvider))]
public void TestMethodUsingMemberDataWhichReferencesAnotherClass(string testCaseName)
{
var testCase = ColorTestDataProvider.BlackAndWhiteTestData[testCaseName];
Assert.Equal(testCase.Red, testCase.Color.R);
Assert.Equal(testCase.Green, testCase.Color.G);
Assert.Equal(testCase.Blue, testCase.Color.B);
}
The need to think of a unique and descriptive name for each test case does add a slight cognotive load, however displaying these names in the test explorer is arguably more readable than displaying the parameter values, particularly if there are a lot of parameters or if they include large arrays.