Building Reliable Software — Unit Testing with C# and XUnit
A reliable software refers to a software that operates error-free, securely, and consistently. It demonstrates robustness across various situations, achieved through testing. Additionally, it meets requirements, is easily maintainable, and provides appropriate user support.
What are Unit tests?
Unit tests are an essential practice in software development, where individual parts of a program are tested to ensure they function correctly in isolation. These individual parts are called “units” and can be functions, methods, or even entire classes, depending on the programming language used. The goal of unit testing is to ensure that each unit of code is functioning as expected, by checking whether inputs produce the correct outputs. This helps identify errors and ensures that any future changes to the code do not break the functioning of previously developed parts.
Benefits of Tested Code
- Error Identification: Code testing helps identify errors, bugs, and flaws in the software. This is essential to ensure that the program functions as expected and doesn’t cause issues for users.
- Quality Improvement: Testing the code enhances the software quality, as it allows you to discover and address issues before the software is delivered to end users.
- Easier Maintenance: Tested code is easier to maintain, as issues are identified and resolved earlier. This reduces the complexity of future fixes and updates.
- Cost Reduction: Identifying and fixing problems early in the development cycle is more cost-effective than dealing with issues after the software’s release.
- Reliability: Code testing increases the reliability of the software, which is crucial for gaining user trust.
The Triple A (Arrange, Act, Assert)
The use of the Arrange, Act, and Assert pattern helps to clearly separate the different stages of the test and makes the test code more readable and understandable, allowing other developers to easily understand what is being tested and how.
- Arrange (Preparation): In this phase, you set up the environment for the test. This involves creating instances of objects, setting initial values, and preparing everything necessary to execute the test. Here, you “arrange” the initial conditions for the test.
- Act (Action): In this phase, you perform the action or operation being tested. This usually involves calling a method or function with the values prepared in the arrangement phase. Here, you “act” by executing the functionality you want to test.
- Assert (Verification): In this phase, you verify if the result of the action matches the expected result. This involves checking if the outputs, states, or behaviors are in line with what you expect. If the verification fails, the test fails. Here, you “assert” that the result is as expected.
Let’s practice!
To do this, we will set up a necessary test scenario. We will use the C# language, .NET 7, and the XUnit library as an example.
For the initial project setup, we can use a ready-made template provided by Microsoft.
There is already a very simple Microsoft tutorial for CLI configuration. Below is the link to the tutorial and also the link to download .NET 7.
Templates: https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-new-sdk-templates
Download .NET: https://dotnet.microsoft.com/download
Below is the code that will create a repository with the necessary configurations.
dotnet new xunit --framework net7.0
Test Scenario
Let’s create the class “Employee” with the field “Name,” and we will also have the method “ValidateName”.
Our goal is to perform tests related to the “ValidateName” method, and for that, we need to understand the different scenarios of outcomes that this method encompasses.
public class Employee
{
public string Name { get; init; }
public bool ValidateName()
{
if (string.IsNullOrWhiteSpace(Name))
return false;
if (Name.Length < 4)
return false;
if (Name.Length > 50)
return false;
return true;
}
}
Let’s first analyze the possible returns of the ValidateName method:
The conditionals often show us different types of return or path that the method can take; they are good indicators of possible tests.
First conditional: it’s validated if the name is null, empty, or composed solely of white spaces.
if (string.IsNullOrWhiteSpace(Name))
⚠️ Notice that the first conditional uses a standard string method in C# string.IsNullOrWhiteSpace(Name). To better understand which scenarios we need to test, we first need to analyze the type of response from this method.
The official documentation states:
Indicates whether a specified string is null, empty, or consists only of white-space characters.
Returns:
true if the value parameter is null or string.Empty, or if value consists exclusively of white-space characters.
So, the first conditional will be true when:
- Name is null,
- Name is empty,
- Name is composed only of white-space characters.
Second conditional: It’s checked whether the name’s length is less than four.
if (Name.Length < 4)
- Third conditional: It’s checked whether the name’s length is greater than 50.
if (Name.Length > 50)
Notice that the only scenario of a true return is if none of these conditionals is true.
Testing!
To start our tests, we will create the EmployeeTests class:
using Xunit;
public class EmployeeTests
{
}
And let’s create a method within the class following the template:
[Fact]
public void MethodName_ArrangeValue_ExpectedResult()
{
//Arrange
var employee = new Employee
{
Name = ... // Arrange Value
};
//Act
var isValid = employee.ValidateName();
//Assert
Assert.True(isValid);
}
The similar creation process for each test scenario:
- Create a method for each test scenario with the
[Fact]
annotation from the Xunit library. - We will always have the Arrange part, where we will fill in the Name.
- We will always have the Act part, where we will call the
ValidateName
method. - And we will always have the Assert part, where we will use some of the Assert class methods from Xunit.
- We can validate if something is true using
Assert.True
, or if something is false usingAssert.False
.
To test the code, we just need to execute the command: dotnet test
- Name with a null value and the expected return is false.
[Fact]
public void ValidateName_NullName_False()
{
//Arrange
var employee = new Employee
{
Name = null
};
//Act
var isValid = employee.ValidateName();
//Assert
Assert.False(isValid);
}
- Name with an empty value and the expected return is false.
[Fact]
public void ValidateName_Empty_False()
{
//Arrange
var employee = new Employee
{
Name = string.Empty
};
//Act
var isValid = employee.ValidateName();
//Assert
Assert.False(isValid);
}
Name composed only of white spaces and the expected return is false.
[Fact]
public void ValidateName_OnlyWhiteSpace_False()
{
//Arrange
var employee = new Employee
{
Name = " "
};
//Act
var isValid = employee.ValidateName();
//Assert
Assert.False(isValid);
}
- Name with a length less than 4 and the expected return is false.
[Fact]
public void ValidateName_3Characters_False()
{
//Arrange
var employee = new Employee
{
Name = "Vic"
};
//Act
var isValid = employee.ValidateName();
//Assert
Assert.False(isValid);
}
Name with a length greater than 50 and the expected return is false.
[Fact]
public void ValidateName_51Characters_False()
{
//Arrange
var employee = new Employee
{
Name = "HMlO8RCy4a9wwmI80UqEVqUyEd39vgLQL3RJEfXjsNUEgAgvqqV"
};
//Act
var isValid = employee.ValidateName();
//Assert
Assert.False(isValid);
}
And finally, a valid name.
[Fact]
public void ValidateName_ValidName_True()
{
//Arrange
var employee = new Employee
{
Name = "Victor Magalhães"
};
//Act
var isValid = employee.ValidateName();
//Assert
Assert.True(isValid);
}
Conclusion
The post discusses unit testing in software development, emphasizing its significance in ensuring the proper functioning of individual code components. It highlights the benefits of testing, such as error identification, quality enhancement, and ease of maintenance. The Triple A method (Arrange, Act, and Assert) is explained, with a focus on its application using C# and XUnit. The post underscores that unit testing is vital for building reliable software and a resilient development process.