In this article, we are going to explore different ways to pass complex parameters to Theory test methods in xUnit.

To download the source code for this article, you can visit our GitHub repository.

Let’s start testing!

Setup to Pass Complex Parameters in xUnit

For this article, we’ll use a simple library system:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
public class Book
{
    public required int Id { get; set; }
    public required string Title { get; set; }
    public required int AuthorId { get; set; }
    public bool IsCheckedOut { get; set; } = false;
}

First, we create a Book class with Id, Title, and AuthorId properties. We also add a IsCheckedOut property to track whether or not it’s checked out from our library.

To learn more about the required keyword, check out our Required Members in C# article.

Then, we move on to the next class:

public class Author
{
    public required int Id { get; set; }
    public required string Name { get; set; }
}

We define the Author class – it’s straightforward with only an Id and a Name properties.

We need a library to hold our books:

public class Library
{
    public List<Book> Books { get; set; } = [];
    public List<Author> Authors { get; set; } = [];

    public bool AddBook(Book book)
    {
        if (book is not null && !Books.Contains(book))
        {
            Books.Add(book);

            return true;
        }

        return false;
    }

    public bool AddAuthor(Author author)
    {
        if (author is not null && !Authors.Contains(author))
        {
            Authors.Add(author);

            return true;
        }

        return false;
    }
}

For this purpose, we create the Library class. It has two properties that represent the books and authors as well as two methods for adding books and authors.

Let’s make use of our Library class:

public class LibraryService(Library library) : ILibraryService
{
    public bool CheckOutBook(int bookId)
    {
        var book = library.Books.Find(b => b.Id == bookId);

        if (book is not null && !book.IsCheckedOut)
        {
            book.IsCheckedOut = true;

            return true;
        }
        else
        {
            return false;
        }
    }

    public bool ReturnBook(int bookId)
    {
        var book = library.Books.Find(b => b.Id == bookId);

        if (book is not null && book.IsCheckedOut)
        {
            book.IsCheckedOut = false;

            return true;
        }
        else
        {
            return false;
        }
    }
}

We create our LibraryService class and utilize a primary constructor to pass a Library instance. It is responsible for checking out and returning books to our Library class.

To learn more about using xUnit, check out our great article Unit Testing with xUnit in ASP.NET Core.

When we start writing tests for our Library and LibraryService classes, we’ll need to be able to pass Book, Author and even Library instances to our test methods. We can’t achieve this with the InlineData attribute as it only works with constant values.

Creating new instances of our classes for each test method is possible, but it will create a lot of duplicated code.

Let’s explore how we can do that!

How to Pass Complex Parameters in xUnit Using the MemberData Attribute

We can use the MemberData attribute to load test data from IEnumerable<object[]> properties:

public class LibraryTests
{
    private readonly Library _library = new();

    public static IEnumerable<object[]> BookData =>
        new List<object[]>
        {
            new object[]
            {
                new Book
                {
                   Id = 1,
                   AuthorId = 1,
                   Title = "Dune"
                }
            }
        };
}

We create a LibraryTests class and initialize a new Library as a field. Then, we create a BookData property that has a type of IEnumerable<object[]> which returns one book that we’ll use for our test.

Let’s create our first test method:

[Theory, MemberData(nameof(BookData))]
public void GivenThatBooksIsNotPresent_WhenAddBookIsInvoked_ThenTrueIsReturned(Book book)
{
    // Act
    var result = _library.AddBook(book);

    // Assert
    result.Should().BeTrue();
}

First, we create a method in which we will verify that the AddBook() method returns true if the book is not in our library.

Next, we decorate with the Theory attribute. We also use the MemberData attribute and pass to it the name of the test data property we just created. As our property returns a list of books, we add that as a parameter to the test method itself.

After this is done, we use the AddBook() method to add the Book instance to the library. Then we assert that the result of the operation should be true. Finally, we can run our test method to see that it passes.

How to Pass Complex Parameters in xUnit Using the ClassData Attribute

There is another attribute called ClassData which we can use to pass test data from a dedicated class:

public class LibraryTestData : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[]
        {
            new Library
            {
                Books = new List<Book>
                {
                    new()
                    {
                       Id = 1,
                       AuthorId = 1,
                       Title = "Dune",
                       IsCheckedOut = false
                    },
                    new()
                    {
                       Id = 2,
                       AuthorId = 2,
                       Title = "Hyperion",
                       IsCheckedOut = true
                    }
                },
                Authors = new List<Author>
                {
                    new()
                    {
                       Id = 1,
                       Name = "Frank Herbert",
                    },
                    new()
                    {
                       Id = 2,
                       Name = "Dan Simmons",
                    }
                }
            }
        };
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

We start by creating a LibraryTestData class that implements the IEnumerable<object[]> interface. In the GetEnumerator() method we create a new Library that holds two books – one that is checked out and one that is not along with two authors.

Next, we create our test class and method:

public class LibraryServiceTests
{
    [Theory, ClassData(typeof(LibraryTestData))]
    public void GivenThatBooksIsCheckedOut_WhenCheckOutBookIsInvoked_ThenFalseIsReturned(Library library)
    {
        // Arrange
        var sut = new LibraryService(library);
        var checkedOutBook = library.Books.First(x => x.IsCheckedOut);

        // Act
        var result = sut.CheckOutBook(checkedOutBook.Id);

        // Assert
        result.Should().BeFalse();
    }
}

We create the LibraryServiceTests class and write one test method. Again we are using the Theory attribute, but instead of MemberData this time, we use ClassData as our second attribute. The ClassData attribute takes in a Type as a parameter, so we use the typeof operator to pass the type of our LibraryTestData class. Then, we pass a Library instance as a parameter to our method.

Inside the method, we initialize a new instance of the LibraryService class by passing the Library instance. Knowing that we have one book already checked out, we retrieve it from the list of books. Next, we try to check out that book again using the CheckOutBook() method. Finally, we assert that the result should be false as this particular book is already checked out.

Tips for Passing Complex Parameters in xUnit

There are several neat tricks we can use when passing complex parameters in xUnit, let’s explore what they are.

Get Test Data by Passing Parameters to a Method Using the MemberData Attribute

Let’s start by creating a method that generates test data:

public static IEnumerable<object[]> GetBookData(int numberOfBooks)
{
    var books = new List<object[]>
    {
        new object[]
        {
            new Book
            {
               Id = 1,
               AuthorId = 1,
               Title = "Dune"
            }
        },
        new object[]
        {
            new Book
            {
               Id = 2,
               AuthorId = 2,
               Title = "Hyperion"
            }
        }
    };

    return books.Take(numberOfBooks);
}

Inside our LibraryTests class, we create the GetBookData() method that takes in an integer, representing how many books we want, and returns an IEnumerable<object[]>.

Next, let’s create a new test method:

[Theory, MemberData(nameof(GetBookData), parameters: 2)]
public void GivenThatBooksIsPresent_WhenAddBookIsInvoked_ThenFalseIsReturned(Book book)
{
    // Arrange
    _library.AddBook(book);

    // Act
    var result = _library.AddBook(book);

    // Assert
    result.Should().BeFalse();
}

Here, we implement a new test that will verify what happens when we try to add a book that is already in our library. We still use the MemberData attribute but this time we pass the name of our method and pass 2 to the parameters property. This will instruct xUnit to run two separate tests with the different test data that the method returns. Using this approach we can use diverse test data to ensure our code works as intended in all situations.

For this test method, we first add the book to the library in the arrange section. Then try to add it again using the AddBook() method, and finally, we assert that the result should be false.

Pass Parameters From Another Class Using the MemberData Attribute

There is one more way to pass complex data to our test and this is to use MemberData attribute to pass parameters from another class:

public class TestData
{
    public static IEnumerable<object[]> AuthorData =>
        new List<object[]>
        {
            new object[]
            {
                new Author
                {
                    Id = 1,
                    Name = "Frank Herbert"

                }
            }
        };
}

Here, we create the TestData class. It has one IEnumerable<object[]> property AuthorData. The property itself returns one Author instance which we can use in our tests.

Then, we use that test data:

[Theory, MemberData(nameof(TestData.AuthorData), MemberType = typeof(TestData))]
public void GivenThatAuthorIsNotPresent_WhenAddBookIsInvoked_ThenTrueIsReturned(Author author)
{
    // Act
    var result = _library.AddAuthor(author);

    // Assert
    result.Should().BeTrue();
}

In our LibraryTests class, we create a new test method. We use the same two attributes, but we pass TestData.AuthorData to the MemberData attribute. Another thing we need to do is set the MemberType property to the type of class that holds our test data. This will instruct xUnit to enter the TestData class and fetch our test data from the AuthorData property.

Inside the test method, we use the AddAuthor() method to add the Author instance to the library. Then we assert that the result of the operation should be true. 

Conclusion

In this article, we showed how passing complex parameters to Theory test methods in xUnit proves essential for comprehensive testing. By leveraging attributes like Member Data and Class Data, we can efficiently manage diverse test data sets without duplicating code. These techniques not only streamline testing processes but also enhance code reliability and maintainability, fostering robust software development practices within the xUnit framework.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!