In this article, we are going to explore different ways to pass complex parameters to Theory test methods in xUnit.
Let’s start testing!
Setup to Pass Complex Parameters in xUnit
For this article, we’ll use a simple library system:
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.
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.
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.