How I Use xUnit’s Theory to Test Switch Statement Expressions

Article summary

Recently for a .NET Core project, my team had to implement a switch statement. This statement had a case for every value of an enum we defined, and each of those returned a specific property on an object. To unit test this, I took advantage of xUnit’s Theory annotation so that I could avoid writing a [Fact] for every case in the switch statement.

The Problem

If you’re unfamiliar with xUnit, the basic unit test is a [Fact], which xUnit describes as for use in testing invariant conditions. Here’s an example that shows how a Divide method should throw an exception. (Note that, in all the following examples, I’m using FluentAssertions in my tests.)


[Fact]
public void Divide_ShouldThrowException_WhenDivisorIsZero()
{
    var action = () => _methods.Divide(1, 0);

    action.Should().Throw().WithMessage("Divisor cannot be zero.");
}

Sometimes when testing, you want to assert multiple sets of inputs work. The [Theory] annotation comes in handy here. It allows us to provide multiple inputs to our test via [InlineData], which will run the test with as many sets of data as we provide. Note that each part of the inlined data is represented by a parameter to the test method, which I then use in the assertion.


[Theory]
[InlineData(1, 2, 2)]
[InlineData(2, 2, 4)]
[InlineData(3, 5, 15)]
[InlineData(0, 1, 0)]
public void Multiply_ShouldReturnResult(int a, int b, int expected)
{
    var result = _methods.Multiply(a, b);

    result.Should().Be(expected);
}

For my project, simply using [InlineData] wasn’t sufficient for our test. We wanted to test a method like this, where given an enum representing a key, we would return a specific property from the object provided in the second parameter. Here’s a basic representation of what that looks like.


public class Methods
{
    public string GetValue(TestEnum key, TestValues values)
    {
        return key switch
        {
            TestEnum.FOO => values.Foo,
            TestEnum.BAR => values.Bar,
            TestEnum.BAZ => values.Baz
        };
    }
}

public enum TestEnum
{
    FOO = 0,
    BAR = 1,
    BAZ = 2
}

public class TestValues
{
    public string Foo { get; set; }
    public string Bar { get; set; }
    public string Baz { get; set; }
}

It would have worked to write an individual [Fact] for each case of the switch statement. But, in the actual project, the number of cases made that an unappealing solution. And given that the object from which we’re getting values could be dynamic, it didn’t feel good to use inlined data to represent the expected values we’d want each case to return.

Our Solution

We ended up taking advantage of a different way to provide test data to a [Theory]. We did that by using [MemberData]. (There is a similar concept in [ClassData], which along with [MemberData] and other xUnit testing strategies, is covered well in this blog post).

Using [MemberData] involved setting up a static property on my test class, which returns an IEnumerable<object[]> via a series of yield return statements. In my case, each returned object has two properties. One is an enum to hit a particular case in the switch statement, and the other is a function that matches the expression to return the correct value for that case.


public class Tests
{
    // constructor omitted

    public static IEnumerable<object[]> GetValueTestCases()
    {
        yield return new object[]
        {
            TestEnum.FOO,
            (TestValues values) => values.Foo
        };
        yield return new object[]
        {
            TestEnum.BAR,
            (TestValues values) => values.Bar
        };
        yield return new object[]
        {
            TestEnum.BAZ,
            (TestValues values) => values.Baz
        };
    }

    // tests omitted
}

Here is the resulting test.


[Theory]
[MemberData(nameof(GetValueTestCases))]
public void GetValue_ShouldReturnCorrectTestValue(TestEnum key, Func<TestValues, string> action)
{
    var values = new TestValues()
    {
        Foo = "Foo Value",
        Bar = "Bar Value",
        Baz = "Baz Value",
    };
    var expected = action(values);

    var result = _methods.GetValue(key, values);

    result.Should().Be(expected);
}

You can see that thanks to the [MemberData], we can provide all our test cases as well as the appropriate function to resolve the expected value.

For the simple example I provided, using [MemberData] is slightly overkill. You could easily test that switch statement by individual [Fact] tests. However, as was the case with my project, using [MemberData] to pass through functions for each test case was a handy tool for testing a pretty big switch statement.

I hope this example of using xUnit’s Theory annotation is helpful for anyone tackling a similar problem

Conversation

Join the conversation

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