Unit Testing in .NET Core - Writing Parameterized Unit Tests with xUnit.net

Unit Testing in .NET Core - Writing Parameterized Unit Tests with xUnit.net

This is the third post in our Unit Testing in .NET Core series! In the previous post, we looked at the xUnit library and wrote our first unit tests using a simple calculator app as our example. In this post, we will look into parameterized unit testing using xUnit.

Introduction to Parameterized Unit Testing

Parameterized unit testing is a testing approach where we define a single test method and supply it with different sets of input parameters. This allows us to test a wide range of scenarios with minimal code duplication. We will explore parameterized unit testing with the classic "FizzBuzz" problem as our example.

The FizzBuzz Problem

Before diving into parameterized unit testing, let's briefly introduce the FizzBuzz problem. FizzBuzz is a simple coding challenge where you print numbers from 1 to n, but for multiples of 3, you print "Fizz," for multiples of 5, you print "Buzz," and for multiples of both 3 and 5, you print "FizzBuzz". Let's start by creating a new class named FizzBuzz.cs.

public class FizzBuzz
{
    public string Generate(int number)
    {
        if (number % 3 == 0 && number % 5 == 0)
            return "FizzBuzz";
        if (number % 3 == 0)
            return "Fizz";
        if (number % 5 == 0)
            return "Buzz";
        return number.ToString();
    }
}

The code is quite straightforward. When given a number:

  • If it's divisible by 3, we print "Fizz."

  • If it's divisible by 5, we print "Buzz."

  • If it's divisible by both 3 and 5, we print "FizzBuzz".

  • Otherwise, we simply print the number itself.

Now let's write the unit tests for the above code.

using Xunit;

public class FizzBuzzTests
{
    [Fact]
    public void Test_Fizz()
    {
        // Arrange
        var fizzBuzz = new FizzBuzz();

        // Act
        var result = fizzBuzz.Generate(3);

        // Assert
        Assert.Equal("Fizz", result);
    }

    [Fact]
    public void Test_Buzz()
    {
        // Arrange
        var fizzBuzz = new FizzBuzz();

        // Act
        var result = fizzBuzz.Generate(5);

        // Assert
        Assert.Equal("Buzz", result);
    }

    [Fact]
    public void Test_FizzBuzz()
    {
        // Arrange
        var fizzBuzz = new FizzBuzz();

        // Act
        var result = fizzBuzz.Generate(15);

        // Assert
        Assert.Equal("FizzBuzz", result);
    }

    [Fact]
    public void Test_Number()
    {
        // Arrange
        var fizzBuzz = new FizzBuzz();

        // Act
        var result = fizzBuzz.Generate(4);

        // Assert
        Assert.Equal("4", result);
    }
}

If we run the tests we can see that all the tests will pass. However, upon closer inspection of the test code, it's evident that the methods share a high degree of similarity. The only variations lie in the method names, the arguments passed into the Generate() method within the "Act" section, and the values against which we perform assertions. The underlying logic remains consistent across all methods. This redundancy sparks an opportunity for improvement. Now, let's see how we can leverage parameterized testing in xUnit to improve this.

Using [Theory] and [InlineData] Attributes

For writing a parameterized test using xUnit we shall use the [Theory] attribute instead of the [Fact] attribute over the test methods. When we use the [Theory] attribute xUnit expects the test data for the test method to be supplied as parameters. The [InlineData] attribute is used to specify the test data for each scenario. Here's how we can rewrite the FizzBuzz tests using [Theory] and [InlineData].

using Xunit;

public class FizzBuzzTests
{
    [Theory]
    [InlineData(3, "Fizz")]
    [InlineData(5, "Buzz")]
    [InlineData(15, "FizzBuzz")]
    [InlineData(4, "4")]
    public void Test_FizzBuzz(int number, string expected)
    {
        // Arrange
        var fizzBuzz = new FizzBuzz();

        // Act
        var result = fizzBuzz.Generate(number);

        // Assert
        Assert.Equal(expected, result);
    }
}

In this version, we use a single method Test_FizzBuzz with parameters as our test method. The [InlineData] attribute supplies the test data, and xUnit automatically executes the test for each set of data.

Note that each [InlineData] will run as a separate unit test. We have just simplified our test suite to a single method instead of separate test methods which improves the maintainability of our tests.

Using [MemberData] Attribute

Another way to provide test data for parameterized tests in xUnit is by using the [MemberData] attribute. This allows us to define a static method or property that returns the test data in the test class itself. The method or property shall return an IEnumerable<object[]> as the test data. Let's convert our FizzBuzz tests to use [MemberData] instead of [InlineData].

using Xunit;

public class FizzBuzzTests
{
    public static IEnumerable<object[]> TestData()
    {
        yield return new object[] { 3, "Fizz" };
        yield return new object[] { 5, "Buzz" };
        yield return new object[] { 15, "FizzBuzz" };
        yield return new object[] { 4, "4" };
    }

    [Theory]
    [MemberData(nameof(TestData))]
    public void Test_FizzBuzz(int number, string expected)
    {
        // Arrange
        var fizzBuzz = new FizzBuzz();

        // Act
        var result = fizzBuzz.Generate(number);

        // Assert
        Assert.Equal(expected, result);
    }
}

In the above code, we create a static method named TestData() which returns an IEnumerable<object[]> containing our test data. In the test method, we used the [MemberData] attribute specifying the name of the TestData method as the parameter. By using [MemberData], we can keep your test data separate from the test method, making it easier to manage and extend our test cases.

Using [ClassData] Attribute

The [ClassData] attribute is an advanced option that allows us to define a class to provide test data. This can be useful when we have complex data requirements. For using a class to provide test data using [ClassData] attribute we need to create a class that inherits from IEnumerable<object[]>. Then we need to implement the GetEnumerator method to return the test data collection.

Let's adapt our FizzBuzz tests to use [ClassData]:

using Xunit;
using System.Collections;
using System.Collections.Generic;

public class FizzBuzzTestData : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] { 3, "Fizz" };
        yield return new object[] { 5, "Buzz" };
        yield return new object[] { 15, "FizzBuzz" };
        yield return new object[] { 4, "4" };
    }

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

public class FizzBuzzTests
{
    [Theory]
    [ClassData(typeof(FizzBuzzTestData))]
    public void Test_FizzBuzz(int number, string expected)
    {
        // Arrange
        var fizzBuzz = new FizzBuzz();

        // Act
        var result = fizzBuzz.Generate(number);

        // Assert
        Assert.Equal(expected, result);
    }
}

In the above code, we created a class FizzBuzzTestData to hold our test data. Inside our test method, we used the [ClassData] attribute with the type of FizzBuzzTestData as the parameter. [ClassData] is particularly useful when you have more complex data generation requirements.

Summary

Parameterized unit testing in xUnit is a powerful technique that simplifies testing by allowing us to reuse a single test method with different sets of input data. We explored three ways to implement parameterized testing using xUnit: [InlineData], [MemberData], and [ClassData]. By adopting these techniques, we can write more maintainable and concise test code while covering a wide range of scenarios in our unit tests. This not only improves code quality but also makes it easier to maintain and extend our test suite as our application evolves.