Saturday, August 15, 2020

xUnit for Unit Testing

xUnit works on both .NET Core and .NET framework projects.

If you are planning to work with .NET Core, then there is a project template in VS 2019. Else if what you have is a .NET framework class library as your unit testing project then you need to install the below two Nugets.











            Common Asserts:

Assert.Equal()
Assert.NotEqual()
Assert.InRange()
Assert.True()
Assert.False()
Assert.Contains()
Assert.DoesNotContain()
Assert.All(collection, item => Assert.False(string.NUllOrWhiteSpace(item)))

Some other Asserts would be:

Assert.IsType<ObjectType>(actualResultingObject);
Assert.IsAssignableFrom<ObjectType>(resultingObject); //To check if the object is of particular type
Assert.Same(objectOne, objectTwo); //Check if two objects are the same
Assert.Throws<ArgumentNullException>(() => obj.Create(null));

In xUnit, unlike NUnit or MSTest, the test class is not attributed. xUnit simply finds public methods in the DLL which are attributed with [Fact] or [Theory]

Structure of a Test method:

class EmployeeShould
{
    [Fact]
    public void GetEmployeeNameById_ShouldReturnCorrectName()
    {
         EmployeeFactory employees = new EmployeeFactory() //Arrange
         var actualValue = employees.GetEmployeeById(001); //Act
         Assert.Euqal("ExpectedName", actualValue); //Assert
    }
}

Categorizing tests
To caegorize test you could use the Trait[] attribute.

Example:
    [Fact]
    [Trait("Category","Employee")]
    public void GetEmployeeNameById_ShouldReturnCorrectName()
    {
    }

You can view the Categories in the Test explorer view. You can also run the categorized test in the command-line interface;

Example: dotnet test --filter Employees

Skipping a test
Example:
    [Fact(Skip = "Dont need to run this test at the moment")]
    public void GetEmployeeNameById_ShouldReturnCorrectName()
    {
    }

Including additional test output text.
For this we need to implement the xUnit's ITestOutputHelper interface

Example:
using Xunit.Abstractions;

public class EmployeeShould
{
    private readonly ITestOutputHelper _output;

    public EmployeeShould(ITestOutputHelper output)
    {
        _output = output;
    }

    [Fact]
    public void GetEmployeeNameById_ShouldReturnCorrectName()
    {
         _output.WriteLine("Starting Employee Test");

         EmployeeFactory employees = new EmployeeFactory();//Arrange
         var actualValue = employees.GetEmployeeById(001); //Act
         Assert.Equal("ExpectedName", actualValue); //Assert
    }
}

You can view the output in the Test explorer. Also when you execute through the command-line, you can view the TRX file result.

Example:  dotnet test --filter Employees --logger:trx

Setup and disposing after test run
Create a constructor in the test class to do the setting up and implement IDisposable interface to dispose.

Example:
public class EmployeeShould : IDisposable
{
    private readonly ITestOutputHelper _output;

    public EmployeeShould(ITestOutputHelper output)
    {
       _employees = new EmployeeFactory(); //Create the object to be used in all test methods 
        _output = output;
    }

    [Fact]
    public void GetEmployeeNameById_ShouldReturnCorrectName()
    {
         _output.WriteLine("Starting Employee Test");

         var actualValue = _employees.GetEmployeeById(001); //Act
         Assert.Euqal("ExpectedName", actualValue); //Assert
    }

    public void Dispose()
    {
        _output.WriteLine("Disposing all EmployeeShould Test class resources");
    }
}

Sharing context between tests
You can use a Fixture class for this purpose. Then implement xUnits IClassFixure interface
Example:

public class CompanyState 
{
    public List<Employee> Employees= new List<Employee>();
    //Add method to load employees from repo

   public void UpgradeEmployeeDesignation()
   {
   }

    public Employee GetEmployeeById(int id)
   {
   }
}

//create new fixture class
public class EmployeeFixture : IDisposable
{
    public CompanyState State { get; private set; }
 
    public EmployeeFixture()
    {
          State = new CompanyState();
    }
   
    //Disposing resources
    public void Dispose()
    {
        //cleanup
    }
}

//implementing the IClassFixture interface in your test class
using Xunit.Abstractions;

public class EmployeeShould : IClassFixure<EmployeeFixture > 
{
    private readonly EmployeeFixture _employeeFixture;
    private readonly ITestOutputHelper _output;

    //constructor
    public EmployeeShould(EmployeeFixture employeeFixtureITestOutputHelper output)
    {
       _employeeFixture = employeeFixture;
        _output = output;
    }

    [Fact]
    public void GetEmployeeNameById_ShouldReturnCorrectName()
    {
         _output.WriteLine("Starting Employee Test");
     
         _employeeFixture.State.UpgradeEmployeeDesignation();  //update employee config across org before test
         var actualValue = _employeeFixture.State.GetEmployeeById(001); //Act
         Assert.Equal("ExpectedDesignation", actualValue.Designation); //Assert
    }
}

Sharing context across multiple test classes
Step 1) Create a fixture class.
Step 2)  Create a new class which implements xUnits ICollectionFixture interface.

Example:
[CollectionDefinition("EmployeeState Collection")]
public class CompanyStateCollection : ICollectionFixture<EmployeeFixture>
{}

Notice here that the CollectionDefinition Attribute is used.

Step 3) Using the collection in the test classes

Example:
//Test class one
[Collection("EmployeeState Collection")]
public class TestClass1
{
    private readonly EmployeeFixture _employeeFixture;
    private readonly ITestOutputHelper _output;

    //constructor
    public EmployeeShould(EmployeeFixture employeeFixtureITestOutputHelper output)
    {
       _employeeFixture = employeeFixture;
        _output = output;
    }

    [Fact]
    public void Test1()
   {}

    [Fact]
    public void Test2()
   {}
}

//Second test class
[Collection("EmployeeState Collection")]
public class TestClass2
{
    private readonly EmployeeFixture _employeeFixture;
    private readonly ITestOutputHelper _output;

    //constructor
    public EmployeeShould(EmployeeFixture employeeFixtureITestOutputHelper output)
    {
       _employeeFixture = employeeFixture;
        _output = output;
    }

    [Fact]
    public void Test3()
   {}

    [Fact]
    public void Test4()
   {}
}

Notice here we have only attributed the collection in test class, and not implemented any interface.

Data Driven Testing


Data driven testing comes in handy for scenarios where the same test method has to be executed for different data inputs.

Example:
Lets say there is a class called Student and it has properties to store the score for each subject. A method to get student grade by deriving the median of all subjects. and another method to get the overall semester pass of fail status though another method. And lets say we need to test the semester pass or fail status. A sample test method for the above mentioned scenario would be something like below:

[Theory]
[InlineData(67,68,69)]
[InlineData(100,100,100)]
[InlineData(100,80,90)]
public void ExamResult_StudentGetsHighMarks_ShouldPassSemester(int math, int science, int english, boolean result)
{
    // test code
}

Theory attribute tells that this test method will be executed multiple times and the InlineData attribute provide the various test data scenarios. The test explorer shows three test cases for the above test method.

How to share test data across tests

In the below sample i will store the test data in a new class file

public class StudentTestData
{
   public static IEnumerable<object[]> TestData
   {
       get
      {
          yield return new object[] {67, 68, 69};
          yield return new object[] {100,100,100};
          yield return new object[] {100,80,90};
      }
   }
}

Now we can update the test method

[Theory]
[DataMember(nameof(StudentTestData.TestData), MemberType = typeof(StudentTestData))]
public void ExamResult_StudentGetsHighMarks_ShouldPassSemester(int math, int science, int english, boolean result)
{
    // test code
}

Reading test data from a file such as a CSV

Sample csv file content:

67,68,69
100,100,100
100,80,90

Create a class to read external test data file:

public class ExternalTestData
{
   public static IEnumerable<object[]> TestData
   {
      get
     {
          string[] csvLines = File.ReadAlLines("TestData.csv");
          var testCases = new List<object[]>();

           foreach(var csvLine in csvLines)
           {
              IEnumerable<int> values = csvLine.Split(',').Select(int.Parse);
              object[] testCase =  values.Cast<object>().ToArray();
              testCases.Add(testCase);
           }
            return testCases;
     }
   }
}

update test method references:

[Theory]
[DataMember(nameof(ExternalTestData.TestData), MemberType = typeof(ExternalTestData))]
public void ExamResult_StudentGetsHighMarks_ShouldPassSemester(int math, int science, int english, boolean result)
{
    // test code
}

If you get an error when you try to run the test such as "File Not Found Exception". Then right click on the CSV file in the project. Go to properties of the CSV. UNder General settings, select "Copy always" in the "Copy to output directory" setting.

Creating Custom Data Source Attributes

We will create a new class which implements xUnits DataAttribute

using Xunit.Sdk;

public class StudentDataAttribute : DataAttribute
{
   public override IEnumerable<object[]> GetData(MethodInfo testMethod)
   {
       yield return new object[] { 67, 68, 69 };
       yield return new object[] { 100, 100, 100 };
       yield return new object[] { 100, 80, 90 };
   }
}

Then update the test method,:

[Theory]
//[DataMember(nameof(ExternalTestData.TestData), MemberType = typeof(ExternalTestData))]
[StudentData]
public void ExamResult_StudentGetsHighMarks_ShouldPassSemester(int math, int science, int english, boolean result)
{
    // test code
}

If you are new to xUnit and have been using other unit testing frameworks you can get an easy comparison between the frameworks by  clicking on this link

No comments:

Post a Comment