Efficient Xunit Theory | MemberData,ClassData,& InlineData | 2023

In the context of the Xunit testing framework, the Theory attribute is used to define a parameterized test method. A parameterized test method is a test method that can accept input parameters, allowing the same test code to be executed multiple times with different input values.

As a developer, I understand how time-consuming it is to write a unit test case. Especially if your technical manager has set an expectation of more than 85% code coverage.

If you want to cover more than 85% of the code, you’ll need to write n test cases to cover all of the validations and scenarios.

If we go with the tedious approach of writing one test case per scenario, you will end up writing hundreds of test cases with just minor changes in the parameters and all copy-pasted.

Efficient Xunit Theory | MemberData,ClassData,& InlineData | 2023 1
Identical objects

Let’s write some pieces of code to demonstrate the use of the theory attribute in the XUNIT framework.

namespace DotNetDemo;
public class Employee
{
    public string GetFullName(string firstName, string secondName)
    {
        ArgumentNullException.ThrowIfNull(firstName);
        ArgumentNullException.ThrowIfNull(secondName);
        return $"{firstName} {secondName}";
    }
}

How many unit test cases can you come up with for the GetFullName method in the Employee class mentioned above? Please pause a moment, then continue reading.

XUnit Theory attribute

  1. Employee_GetFullName_When_FirstString_IsNull_Throw_ArgumentNullException
  2. Employee_GetFullName_When_FirstString_IsEmpty_Throw_ArgumentNullException
  3. Employee_GetFullName_When_FirstString_HasWhiteSpaces_Throw_ArgumentNullException
  4. Employee_GetFullName_When_SecondString_IsNull_Throw_ArgumentNullException
  5. Employee_GetFullName_When_SecondString_IsEmpty_Throw_ArgumentNullException
  6. Employee_GetFullName_When_SecondString_HasWhiteSpaces_Throw_ArgumentNullException
  7. Employee_GetFullName_Should_Have_Space_Between_FirstName_And_LastName
  8. Employee_GetFullName_ShouldNot_Have_Space_After_LastName
  9. Employee_GetFullName_ShouldNot_Have_Space_Before_FirstName

So, in total, nine test cases that I could come up with for the getfullname method in the employee class. If we follow the typical approach of writing the test cases, then we must have totally nice test cases.

Fact Attribute in XUnit

Let’s Write all the above test cases using the XUNIT framework and execute them. Every unit testing framework has its own set of attributes to decorate the methods and classes.

Using the Fact attribute in the Xunit you can mark the methods which have no parameter as test cases that will be executed by the unit test case runner.

If you try to pass a parameter into the Fact, You will get a compile-time warning like this.

Efficient Xunit Theory | MemberData,ClassData,& InlineData | 2023 2
Fact Method cannot have parameters in XUnit

Let’s see what all the test cases look like after implementation.

namespace DotNetDemo.UnitTests;
public class EmployeeTests
{
    public Employee UnitUnderTest = new Employee();
    [Fact]
    public void Employee_GetFullName_When_FirstString_IsNull_Throw_ArgumentNullException()
    {
        var Act = () => UnitUnderTest.GetFullName(null, "K");
        Assert.Throws<ArgumentNullException>(Act);
    }
    [Fact]
    public void Employee_GetFullName_When_FirstString_IsEmpty_Throw_ArgumentNullException()
    {
        var Act = () => UnitUnderTest.GetFullName("", "K");
        Assert.Throws<ArgumentNullException>(Act);
    }
    
    [Fact]
    public void Employee_GetFullName_When_FirstString_HasWhiteSpaces_Throw_ArgumentNullException()
    {
        var Act = () => UnitUnderTest.GetFullName("    ", "K");
        Assert.Throws<ArgumentNullException>(Act);
    }
    
    [Fact]
    public void Employee_GetFullName_When_SecondString_IsNull_Throw_ArgumentNullException()
    {
        var Act = () => UnitUnderTest.GetFullName("D", null);
        Assert.Throws<ArgumentNullException>(Act);
    }
    
    [Fact]
    public void Employee_GetFullName_When_SecondString_IsEmpty_Throw_ArgumentNullException()
    {
        var Act = () => UnitUnderTest.GetFullName("D", "");
        Assert.Throws<ArgumentNullException>(Act);
    }
    
    [Fact]
    public void Employee_GetFullName_When_SecondString_HasWhiteSpaces_Throw_ArgumentNullException()
    {
          var Act = () => UnitUnderTest.GetFullName("D", "  ");
                Assert.Throws<ArgumentNullException>(Act);
    }
    [Fact]
    public void Employee_GetFullName_Should_Have_Space_Between_FirstName_And_LastName()
    {
        var fullName = UnitUnderTest.GetFullName("Deependra", "Kush");
        var nameArray = fullName.Split(" ");
        
        Assert.NotEmpty(nameArray);
        Assert.Equal(2, nameArray.Length);
        Assert.Equal("Deependra",nameArray[0]);
        Assert.Equal("Kush",nameArray[1]);
    }
    [Fact]
    public void Employee_GetFullName_ShouldNot_Have_Space_Before_FirstName()
    {
        var fullName = UnitUnderTest.GetFullName("Deependra", "Kush");
        var nameArray = fullName.Split(" ");
        
        Assert.NotEmpty(nameArray);
        Assert.Equal(2, nameArray.Length);
        Assert.Equal("Deependra",nameArray[0]);
    }
    
    [Fact]
    public void Employee_GetFullName_ShouldNot_Have_Space_After_LastName()
    {
        var fullName = UnitUnderTest.GetFullName("  Deependra  ", "   Kush  ");
        var nameArray = fullName.Split(" ");
        
        Assert.NotEmpty(nameArray);
        Assert.Equal(2, nameArray.Length);
        Assert.Equal("Kush",nameArray[1]);
    }
  
}

After a successful run of the above test cases.

Efficient Xunit Theory | MemberData,ClassData,& InlineData | 2023 3

XUnit Theory vs Fact Attribute

As we have seen in the above example, it’s not possible to pass the parameters to the unit test case using the Fact attribute.

We must use the XUnit theory attribute in order to pass the parameters to the test cases.

Both the Attributes [Fact] and [Theory] are defined by xUnit.net.

The xUnit.net test runner uses the [Fact] attribute to distinguish between a “normal” unit test and a test method that doesn’t accept method arguments.

On the other hand, the Theory attribute anticipates one or more DataAttribute instances to provide the values for the method arguments of a Parameterized Test.

There are multiple ways of passing an argument to the theory methods.

  • InlineData
  • MemberData
  • ClassData

XUnit InlineData Attribute

XUnit InlineData attribute can be used along with the theory attribute to pass simple parameters to the test case.

By decorating a test method with InlineData, you can provide multiple sets of input values and expected outputs in a concise and readable manner. This attribute eliminates the need for separate test cases and simplifies the test code. With InlineData, you can quickly define and execute multiple test scenarios, ensuring comprehensive test coverage and efficient testing of your code.

It takes the same number of parameters as expected in the unit test case. eg: in the below unit test case takes one parameter & Inline data also has one parameter.

    [Theory]
    [InlineData("")]
    [InlineData(" ")]
    [InlineData(null)]
    public void Employee_GetFullName_Throw_ArgumentNullException_When_FirstName_Is(string firstName)
    {
        var Act = () => UnitUnderTest.GetFullName(firstName, "K");
        Assert.Throws<ArgumentNullException>(Act);
    }

    [Theory]
    [InlineData("")]
    [InlineData(" ")]
    [InlineData(null)]
    public void Employee_GetFullName_Throw_ArgumentNullException_When_SecondName_Is(string secondName)
    {
        var Act = () => UnitUnderTest.GetFullName("Deependra",secondName);
        Assert.Throws<ArgumentNullException>(Act);
    }

    [Theory]
    [InlineData("","K")]
    [InlineData("D"," ")]
    [InlineData(null,null)]
    [InlineData("","")]
    public void Employee_GetFullName_Throw_ArgumentNullException_When_FirstName_Or_SecondName_Is_Invalid(string firstName,string secondName)
    {
        var Act = () => UnitUnderTest.GetFullName(firstName,secondName);
        Assert.Throws<ArgumentNullException>(Act);
    }

After executing the above test case we will be covering the same number of test cases that we did earlier using the Fact Attribute.

However, we have reduced the redundant code and time taken to implement the multiple test case with some minor differences.

Efficient Xunit Theory | MemberData,ClassData,& InlineData | 2023 4
XUnit test cases output using Theory attribute

In the InlineData attribute, we can only pass the same number of arguments accepted by a test method. If we will try to do that, the compiler will notify us.

Efficient Xunit Theory | MemberData,ClassData,& InlineData | 2023 5
Parameter mismatch in InlineData and test case method

Xunit Memberdata

xUnit’s MemberData attribute is a powerful feature that enables data-driven testing in C# applications. By leveraging MemberData, you can provide different sets of test data to a single test method, allowing you to efficiently test a wide range of scenarios.

This attribute works by specifying a property or method that returns the test data, which can be dynamically generated or fetched from various sources such as databases, CSV files, or APIs.

With MemberData, you can easily separate test data from test logic, making your tests more maintainable and reducing code duplication.

This feature promotes a data-centric approach to testing, where you can focus on defining different inputs and expected outputs for your tests, leading to more comprehensive test coverage.

By utilizing xUnit’s MemberData attribute, you can enhance the flexibility, readability, and scalability of your test suites, ultimately improving the quality and reliability of your C# applications.

public static IEnumerable<object[]> NamesData =>
    new List<object[]>
    {
        new object[] { "" },
        new object[] { " " },
        new object[] { null },
    };
[Theory]
[MemberData(nameof(NamesData))]
public void Employee_GetFullName_Throw_ArgumentNullException_When_FirstName_Is(string firstName)
{
    var Act = () => UnitUnderTest.GetFullName(firstName, "K");
    Assert.Throws<ArgumentNullException>(Act);
}

As shown in the previous example, NamesData is a static property that can be used inside the Xunit MemberData attribute to provide parameter values during test case execution.

Using MemberData, We can pass multiple parameter values. Needs to just add the additional values to the object array.

Let’s add some additional values to the NamesData method or maybe create another MemberData method that has more than one parameter to be passed.

MemberData Private Method with multiple values
MemberData Private Method with multiple values

We can pass any number of parameters using a similar approach and use that MemberData property in our theory test case.

public static IEnumerable<object[]> FirstNamesAndLastNames =>
            new List<object[]>
            {
                new object[] { "", "K" },
                new object[] { "Deep", " " },
                new object[] { null, "K" },
                new object[] { "Deep", null },
            };

   [Theory]
        [MemberData(nameof(FirstNamesAndLastNames))]
        public void Employee_GetFullName_Throw_ArgumentNullException_When_FirstName_And_LastName_Is(string firstName,
            string lastname)
        {
            var Act = new Func<string>(() => _unitUnderTest.GetFullName(firstName, lastname));
            Assert.Throws<ArgumentNullException>(Act);
        }
The output of XUnit test cases by passing multiple values to parameters using memberdata
The output of XUnit test cases by passing multiple values to parameters using MemberData

XUnit ClassData Attribute

The ClassData attribute in xUnit is used to provide test data from a separate class to a test method. It allows you to define a separate class that acts as a data source for your test cases, enabling you to easily parameterize and generate test data.

By applying the ClassData attribute to your test method and specifying the data class, xUnit will automatically invoke the data class and pass the generated data to your test method.

This approach promotes code reusability and simplifies the management of test data, making it a powerful feature for data-driven testing in xUnit.

You can keep the common data in a dedicated class and use the same class across multiple test methods.

  public class EmployeeTestData:IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] { new Employee { Id = 1, FirstName = "John", LastName = "" } };
            yield return new object[] { new Employee { Id = 2, FirstName = "Mary", LastName = null } };
            yield return new object[] { new Employee { Id = 3, FirstName = "Mary", LastName = null } };
            yield return new object[] { new Employee { Id = 4, FirstName = "", LastName = null } };
            yield return new object[] { new Employee { Id = 5, FirstName = "", LastName = "john" } };
            yield return new object[] { new Employee { Id = 6, FirstName = null, LastName = " " } };
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

In the above class, we have implemented the IEnumerable interface to return the list of Employee objects that can be used as test data for one of our test cases.

        [Theory]
        [ClassData(typeof(EmployeeTestData))]
        public void Employee_GetFullName_Throw_ArgumentNullException_When_FirstName_Is(Employee employee)
        {
            var Act = new Func<string>(() => _unitUnderTest.GetFullName(employee.FirstName, employee.LastName));
            Assert.Throws<ArgumentNullException>(Act);
        }
Efficient Xunit Theory | MemberData,ClassData,& InlineData | 2023 6
XUnit test case output with ClassData attribute

Conclusion

Using the XUnit Theory attribute can be utilized to improve the complete development effort by reducing the number of test cases we have to write and also it makes the code cleaner and more readable.

We have gone through the examples of different attributes MemberData, InlineData, and ClassData that can be used along with the Theory attribute to make the test cases easier and cleaner.

Comments are closed.

Scroll to Top