It should be clear to everyone by now that automating code testing is not a whim, a habit of some programmers, nor an optional activity. It is a necessity that helps you to be confident that your code does what it should, and allows you to change it without fear.
Although there are many different approaches out there, when talking about testing code most of us usually just think of unit tests, which is a type of test that takes into account an isolated portion of code to verify its functionalities.
Of course, an application may have different aspects that can be automatically verified, such as the interaction between different components or subsystems, or even the user interaction with the UI. All these aspects are tackled with specific types of tests, like integration tests, UI tests, and end-to-end tests (e2e tests).
While unit tests verify the behavior of an autonomous portion of code, integration tests check the behavior of a combination of code units built with multiple interdependent components. Sometimes those components may involve a database, file system or other similar structure.
On the other hand, e2e tests are specific integration tests that verify the behavior of complete user functionality.
Despite the variety of possible test types, unit tests are the starting point when it comes to automated tests. Every developer should know how to create them within the ecosystem of their programming language.
Introducing xUnit
This article takes a look at how you can create unit tests for your C# code using xUnit, one of the most popular test frameworks for the .NET ecosystem. This open-source library is part of the .NET Foundation and can be used with any .NET programming language. It is already part of the more recent versions of .NET Core, so you don’t need to install anything but the .NET Core SDK.
Please don’t confuse the xUnit testing library we are going to explore in this article with the set of testing frameworks that unfortunately carries the same name! We will discuss the latter at a later point.
In order to show the features of xUnit, let’s assume you have a class library with the following code:
This class exposes the public method IsValidAddress()
that validates an email address. Of course, better approaches exist to validate an email, but these are irrelevant for our purpose here. We are going to set up some unit tests with xUnit to ensure that this method is correct.
To do this, you need to add a new project to your .NET solution, based on the xUnit template. If you are using the .NET CLI, you should simply run the following command:
dotnet new xunit
Code language: JavaScript (javascript)
If you are using Visual Studio, just select the xUnit project template when adding a new project to the current solution.
Creating Facts
One of the main reasons for the popularity of xUnit is its essentiality. In fact, a test suite in xUnit is just a standard class with methods implementing each unit test. Let’s take a look at how to verify that the IsValidAddress() method is able to do its work. As is normally the case, in the following code we are applying the Arrange, Act, Assert pattern to structure our unit test:
As you can see, we have defined the ValidEmail() method and applied the Fact attribute to it.
A fact in xUnit is a sort of assertion about a condition – a declaration that must be true. From the syntactical point of view, it is a method decorated by the Fact attribute that contains an invocation of at least one method of the Assert object. In the example above, we arranged the unit test by creating an instance of the MailManager class and assigning a valid email address to the mailAddress variable. Then, we called the IsValidAddress() method and checked the result by using the Assert.True() method.
In this specific case, the Assert.True() method takes two arguments: a condition to be expected as true, and a message to display when that condition is false.
The Assert object provides many other methods to check the result of the Act step in a unit test: Assert.Equal(), Assert.NotEqual(), Asset.Null(), Assert.NotNull(), to mention only those most commonly used.
Coming back to our unit test example, it is a good practice to test negative cases as well. For example, you should verify if the IsValidAddress() method correctly evaluates an invalid email address. The following is the code that accomplishes this task:
Apart from the different name of the unit test method, you will notice that we have used the Assert.False() method here. Our expectation is that this time the IsValidAddress()
method will return a false
value.
To run these tests, you can use the test runner in Visual Studio, or type the following command in a terminal window:
dotnet test
Using theories
The unit test examples shown above give you an idea of how simple writing unit tests in xUnit is. However, the test cases covered by the two unit tests are not enough to affirm that the IsValidAddress()
method is acting as expected. There are many other cases with valid or invalid addresses. For example, the string johnsmith@company.it is a valid email address, while the empty string is not a valid address.
You may consider creating a unit test for each test case. This approach would work, but it is not practical; it would involve too much repeated code with changes only occurring in the input string.
Luckily, xUnit helps you with theories. A theory is a parametric unit test that allows you to represent a set of unit tests that share the same structure. In our case, we can leverage theories by writing the following code:
Here you find the CheckMail()
method decorated with a few attributes. The Theory attribute informs the xUnit runner that this is a theory, not a simple fact. The InlineData
attributes define a set of data to be passed to the CheckMail()
method. Unlike the fact-based unit tests, a theory unit test may have one or more parameters. In this case, the CheckMail()
method has two parameters: the mailAddress
parameter is the string to be evaluated, while the expectedTestResult
is the result we expect to get by calling the IsValidAddress()
method.
The InlineData
attributes allow you to pass a set of input strings, with their respective expected results, to the CheckMail()
method.
This way, you can combine a set of similar but specific unit tests within one unit test .
TDD, BDD, and xUnit
You now have a basic, but practical knowledge of the xUnit testing framework. At this point, you may wonder how xUnit fits with Test Driven Development (TDD) or Behavior Driven Development (BDD). Both are software design practices that emphasize the role of testing as the starting point of the development process.
In particular, TDD promotes writing tests before the code to be tested is itself written. This practice enables an iterative development cycle where a test is written, fails, and is then fixed.
BDD is on the same page, and can be considered a branch of TDD, since it also highlights the need to write the tests before the application code. In addition, BDD proposes using a human-readable syntax to define tests in a format that imitates user requirements.
Understandably, xUnit doesn’t dictate which practice to use when writing your tests. This means that you can use xUnit within the practice of TDD. However, BDD’s focus on a human-centric syntax is not easily achieved within the simple syntax of xUnit. To leverage BDD, you should use specific tools, like SpecFlow. This BDD tool lets you define different behavior scenarios by writing tests.
xUnit and other .NET testing frameworks
While xUnit has gained popularity in recent years, other testing frameworks are available in the .NET ecosystem. Beside MSTest – Microsoft’s testing framework, bundled with Visual Studio – NUnit is the historical unit testing tool that has been standard in the .NET environment since early 2000. Initially, NUnit was a porting of JUnit, the unit testing framework for Java developers. NUnit, along with JUnit, was part of a set of testing tools with a common architecture, known as xUnit, where x stands for the first letter of the programming language or framework. Unfortunately, this set of tools has the same name as the xUnit library, but they two must not be confused.
NUnit is a mature tool, well documented, and widely adopted. It supports parallel execution of unit tests with a class-based isolation level, unlike xUnit, which supports a method-based isolation level. xUnit is also more modern and intuitive, and its growing popularity is due primarily to its simplicity, expressiveness, and extensibility. This latter feature is particularly important as it allows the creation of attributes like Fact and Theory.
You may also need other types of tools for your tests. For example, when writing integration tests, you may need to simulate your dependencies, such as a third-party library, a complex component, or an external system. In these cases, using the actual dependency may add a lot of extra effort in terms of both performance and complexity. The standard solution is to mock the dependency; that is, to create a light class with the same interface expected of the dependency, but a simulated behavior. You can write your own mock classes and use them in your unit tests, but this results in more code to write and maintain. Alternatively, you can use a library of mock classes such as Moq or NSubstitute.
Conclusion
This article has provided you with a high-level overview of software testing, while focusing on xUnit unit tests. After a quick classification of the different types of tests, you learned how to use facts and theories in order to effectively write tests using the xUnit library. Of course, xUnit also provides you with many other features that allow you to organize your tests better.
Creating automated tests should be a standard activity for a developer – part of the development task itself, not an optional extra step. In addition, unit tests are just one of the types of test you can perform on an application. As discussed above, other types of test are needed when your application grows, interacts with other components, is part of a complex process, and so on. Each type of test may need a specific tool, but unit tests can be considered the pillars upon which a testing infrastructure can built.