For more complex test cases, we can use theories. A theory contains two parts:
- A [Theory] attribute that marks the method as a theory.
- At least one data attribute that allows passing data to the test method: [InlineData], [MemberData], or [ClassData].
When writing a theory, your primary constraint is ensuring that the number of values matches the parameters defined in the test method. For example, a theory with one parameter must be fed one value. We look at some examples next.
You are not limited to only one type of data attribute; you can use as many as you need to suit your needs and feed a theory with the appropriate data.
The [InlineData] attribute is the most suitable for constant values or smaller sets of values. Inline data is the most straightforward way of the three because of the proximity of the test values and the test method.Here is an example of a theory using inline data:
public class InlineDataTest
{
[Theory]
[InlineData(1, 1)]
[InlineData(2, 2)]
[InlineData(5, 5)]
public void Should_be_equal(int value1, int value2)
{
Assert.Equal(value1, value2);
}
}
That test method yields three test cases in the Test Explorer, where each can pass or fail individually. Of course, since 1 equals 1, 2 equals 2, and 5 equals 5, all three test cases are passing, as shown here:

Figure 2.4: Inline data theory test results
We can also use the [MemberData] and [ClassData] attributes to simplify the test method’s declaration when we have a large set of data to tests. We can also do that when it is impossible to instantiate the data in the attribute. We can also reuse the data in multiple test methods or encapsulate the data away from the test class.Here is a medley of examples of the [MemberData] attribute usage:
public class MemberDataTest
{
public static IEnumerable<object[]> Data => new[]
{
new object[] { 1, 2, false },
new object[] { 2, 2, true },
new object[] { 3, 3, true },
};
public static TheoryData<int, int, bool> TypedData =>new TheoryData<int, int, bool>
{
{ 3, 2, false },
{ 2, 3, false },
{ 5, 5, true },
};
[Theory]
[MemberData(nameof(Data))]
[MemberData(nameof(TypedData))]
[MemberData(nameof(ExternalData.GetData), 10, MemberType = typeof(ExternalData))]
[MemberData(nameof(ExternalData.TypedData), MemberType = typeof(ExternalData))]
public void Should_be_equal(int value1, int value2, bool shouldBeEqual)
{
if (shouldBeEqual)
{
Assert.Equal(value1, value2);
}
else
{
Assert.NotEqual(value1, value2);
}
}
public class ExternalData
{
public static IEnumerable<object[]> GetData(int start) => new[]
{
new object[] { start, start, true },
new object[] { start, start + 1, false },
new object[] { start + 1, start + 1, true },
};
public static TheoryData<int, int, bool> TypedData => new TheoryData<int, int, bool>
{
{ 20, 30, false },
{ 40, 50, false },
{ 50, 50, true },
};
}
}
The preceding test case yields 12 results. If we break it down, the code starts by loading three sets of data from the Data property by decorating the test method with the [MemberData(nameof(Data))] attribute. This is how to load data from a member of the class the test method is declared in.Then, the second property is very similar to the Data property but replaces IEnumerable<object[]> with a TheoryData<…> class, making it more readable and type-safe. Like with the first attribute, we feed those three sets of data to the test method by decorating it with the [MemberData(nameof(TypedData))] attribute. Once again, it is part of the test class.
I strongly recommend using TheoryData<…> by default.