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 percent code coverage.

The Xunit theory attribute provided in the XUNIT framework can be used to pass the dynamic parameters to the unit test cases. which can save a lot of time and make the test cases cleaner.

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.

XUnit Theory With Memberdata, ClassData & Inlinedata | Improve Productivity | 2022 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.

XUnit Theory With Memberdata, ClassData & Inlinedata | Improve Productivity | 2022 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.

XUnit Theory With Memberdata, ClassData & Inlinedata | Improve Productivity | 2022 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.

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.

XUnit Theory With Memberdata, ClassData & Inlinedata | Improve Productivity | 2022 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.

XUnit Theory With Memberdata, ClassData & Inlinedata | Improve Productivity | 2022 5
Parameter mismatch in InlineData and test case method

Xunit Memberdata

Xunit MemberData attribute can be used for loading the complex data for the test cases. Any static property or method can be assigned to the XUnit memberdata attribute.

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

If the test case takes the type as a parameter we can use the ClassData attribute. Also, One of the advantages of using the ClassData attribute is clean code and reusability.

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);
        }
XUnit Theory With Memberdata, ClassData & Inlinedata | Improve Productivity | 2022 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.

0 0 votes
Article Rating
4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
trackback
6 months ago

[…] you are a big fan of writing unit test cases, you must have implemented this for mock data […]

trackback
6 months ago

[…] XUnit follows the clean code approach and does not have any attribute-based methods to set up and cleanup activities. […]

trackback
6 months ago

[…] for holding the different types of data types. In .NET concepts of tuple were introduced in the .Net framework 4 by Tupple<T> generic […]

trackback
4 months ago

[…] ArgumentNullException when we try to add the null as a key in the dictionary. […]