Strongly typed xUnit theory test data with TheoryData

Reading Time: 5 minutes

TheoryData class

In xUnit, test methods can be decorated with attributes like [Fact] or [Theory]. When a test method is decorated with the [Theory] attribute, it means that the method is not testing a single case but instead testing a general rule that should hold true for multiple sets of data.

TheoryData is a class in the xUnit.net testing framework that is used to provide data for theory tests. The TheoryData class is similar to a list or a dictionary, in that it allows you to add and remove data items.

The TheoryData<> types provide a series of abstractions around the IEnumerable<object[]> required by theory tests. It consists of a TheoryData base class, and a number of generic derived classes TheoryData<>

To use TheoryData, you create an instance of the class and then add sets of data to it using the Add method. Each set of data consists of one or more arguments that are passed to the theory test method. The number and types of arguments depend on the parameters of the theory test method.

Using TheoryData with the [MemberData] attribute

Let’s start defining a Person class:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Country { get; set; }
}

Then it follows a static class called PersonTestData that contains two public properties that each return a TheoryData object. A TheoryData object is used to pass data to a theory, which is a test method that can be executed with multiple data sets.

The first TheoryData property is called IsLessThan30YearsOld and contains three test cases, where each case includes a Person object and a boolean value that indicates whether the person’s age is less than or equal to 30.

The second TheoryData property is called PatchCountry and also contains two test cases, where each case includes two Person objects that represent the original Person and a Person with a country field that has been patched.

using Xunit;

namespace MemberData.Tests;

public static class PersonTestData
{
    public static TheoryData<Person, bool> IsLessThan30YearsOld =>
        new TheoryData<Person, bool>
        {
            {
                new()
                {
                    Name = "Antonio",
                    Age = 28,
                },
                true
            },
            {
                new ()
                {
                    Name = "Mario",
                    Age = 32
                },
                false
            },
            {
                new ()
                {
                    Name = "Alice",
                    Age = 35
                },
                false
            }
        };

    public static TheoryData<Person, Person> PatchCountry =>
        new TheoryData<Person, Person>
        {
            {
                new()
                {
                    Name = "Antonio",
                    Age = 28,
                },
                new()
                {
                    Name = "Antonio",
                    Age = 28,
                    Country = "US"
                }
            },
            {
                new()
                {
                    Name = "Antonio",
                    Age = 28,
                    Country = "Italy"
                },
                new()
                {
                    Name = "Antonio",
                    Age = 28,
                    Country = "Italy"
                }
            }
        };
}

To provide the data sets for the test method, you can use the [MemberData] attribute, which allows you to specify a property or method that returns a collection of data. In the code provided below, we can see two test methods that use the [Theory] attribute and the [MemberData] attribute to receive data from the PersonTestData class.

using Newtonsoft.Json;
using Xunit;

namespace MemberData.Tests;

public class PersonTest
{
    [Theory]
    [MemberData(nameof(PersonTestData.IsLessThan30YearsOld), MemberType = typeof(PersonTestData))]
    public void GivenPerson_WhenAgeIsSmallerOrEqual30_ThenReturnsTrue(Person person, bool expected)
    {
        // GIVEN: a person with an age
        // WHEN: the age is tested
        var result = person.Age <= 30;

        // THEN: the result is equal to the expected value
        Assert.Equal(expected, result);
    }

    [Theory]
    [MemberData(nameof(PersonTestData.PatchCountry), MemberType = typeof(PersonTestData))]
    public void GivenPerson_WhenCountryIsNotAssigned_ThenShouldPatchPerson(Person person, Person personPatched)
    {
        // GIVEN: a person without a country
        // WHEN: the country is patched
        person.Country ??= "US";

        // THEN: the patched person is equal to the expected person
        Assert.Equal(
              JsonConvert.SerializeObject(person, new JsonSerializerSettings()),
              JsonConvert.SerializeObject(personPatched, new JsonSerializerSettings()),
              ignoreCase: true,
              ignoreLineEndingDifferences: true,
              ignoreWhiteSpaceDifferences: true);
    }
}

The code is a simple xUnit.net test class that contains two test methods. The first method tests whether a given person is less than 30 years old. The second method tests whether a person’s country is patched correctly when the country is not assigned.

Both test methods use the Theory attribute to indicate that they are theory tests. The MemberData attribute is used to specify the data that is passed to the test method. The data is defined in the PersonTestData class, which contains two TheoryData collections: IsLessThan30YearsOld and PatchCountry.

TheoryData<> vs IEnumerable<object[]>

Using TheoryData instead of IEnumerable<object[]> has some benefits that can make tests more convenient and easier to maintain.

  1. TheoryData can define the types of each set of data, which can be useful to ensure that the test method receives the expected types of arguments. In the example provided, the IsLessThan30YearsOld collection has a type of TheoryData<Person, bool>, which indicates that the first argument of each set of data should be a Person object and the second argument should be a boolean value. With IEnumerable<object[]>, it can be harder to ensure that the correct types of arguments are being passed to the test method.
  2. TheoryData is more flexible and expressive than IEnumerable<object[]>. TheoryData allows you to define collections of different types and even collections of collections. In contrast, IEnumerable<object[]> is more limited in its ability to define collections of complex types.
  3. TheoryData has built-in support for null values, while IEnumerable<object[]> does not. When using IEnumerable<object[]>, you need to manually check for null values and handle them accordingly, which can be cumbersome.
  4. TheoryData can be used in a fluent style, which can make it more readable and maintainable. You can add sets of data to a TheoryData collection using the Add method, which allows you to chain multiple Add calls together to define the collection. This can make the code more readable and easier to modify.
  5. TheoryData provides better error messages than IEnumerable<object[]>. When a test fails, the error message in TheoryData is more detailed and includes the values of the arguments that caused the test to fail. In contrast, the error message in IEnumerable<object[]> is less detailed and can be harder to understand.

In summary, TheoryData provides a more flexible, expressive, and maintainable way to define collections of data for test methods in xUnit, which can make tests easier to write and maintain.

Leave a Comment

Your email address will not be published. Required fields are marked *