This is the first article from the series where we are going to present different topics related to unit testing with xUnit in ASP.NET Core.
Before we start with unit testing with xUnit, we are going to give a brief overview of the xUnit tool and our starting project.
Later on, we are going to add a new class with validation logic and finally, learn how to test that new functionality with the xUnit project.
For the complete navigation of this series, you can visit ASP.NET Core Testing.
So, let’s dive right into it.
About xUnit – Does xUnit Support .NET Core
xUnit is a free, open-source, testing tool for .NET which developers use to write tests for their applications. It is essentially a testing framework that provides a set of attributes and methods we can use to write the test code for our applications. Some of those attributes, we are going to use are:
- [Fact] – attribute states that the method should be executed by the test runner
- [Theory] – attribute implies that we are going to send some parameters to our testing code. So, it is similar to the [Fact] attribute, because it states that the method should be executed by the test runner, but additionally implies that we are going to send parameters to the test method
- [InlineData] – attribute provides those parameters we are sending to the test method. If we are using the [Theory] attribute, we have to use the [InlineData] as well
Regarding the xUnit support – it provides support for .NET Core 1.0 and later. Additionally, it supports both .NET 5 and .NET 6, which is the framework we are going to write our tests in. It also supports .NET Framework 4.5.2 or later.
As we said, xUnit provides us with a lot of assertion methods that we use to validate our production code. As we progress through this series, we are going to use different assertion methods to test different production functionalities.
Once we write our test method, we need to run it to be sure whether it works or not. So to run a unit test in .NET Core, we are going to use Visual Studio’s Test Explorer, by opening the Test menu and then Windows > Test Explorer. We can use a keyboard shortcut as well: CTRL+E, T.
Overview of the Starting Project
We have created a starting project to start this series off faster. We strongly recommend downloading and using it in the rest of the series.
Once we open the project, we can inspect a solution explorer.
We can see that we have a repository class for the repository logic with its IEmployeeRepository
interface. This will be quite important for us once we start writing tests for our controller in future articles.
Our controller contains three actions, one for the GET request and two for the POST request. We can see views for Index and Create actions as well.
Finally, we can see the Migrations folder, which contains migration files for our series. So, in order for you to use the prepared data, you have to change a connection string in the appsettings.json
file and just run the project. It will automatically create a database and seed all the required data.
Now, that we are familiar with the starting project, we can move on to the next phase by adding an additional class with validation logic.
Preparing Validation for Unit Testing with xUnit
Before we start, let’s take a look at our Employee
entity class:
[Table("Employee")] public class Employee { public Guid Id { get; set; } [Required(ErrorMessage = "Name is required")] public string? Name { get; set; } [Required(ErrorMessage = "Age is required")] public int Age { get; set; } [Required(ErrorMessage = "Account number is required")] public string? AccountNumber { get; set; } }
And the HttpPost
action in the controller:
[HttpPost] [ValidateAntiForgeryToken] public IActionResult Create([Bind("Name,AccountNumber,Age")] Employee employee) { if(!ModelState.IsValid) { return View(employee); } _repo.CreateEmployee(employee); return RedirectToAction(nameof(Index)); }
In the Create
action, we are adding a new employee object to the database if the model is valid.
But now, we’ve decided to add additional validation for the AccountNumber
property. To do that, we need to create a new validation class and after that write tests for each validation rule inside that class.
So, let’s start by adding a new folder named Validation
and inside it a new class AccountNumberValidation
:
public class AccountNumberValidation { private const int startingPartLength = 3; private const int middlePartLength = 10; private const int lastPartLength = 2; public bool IsValid(string accountNumber) { var firstDelimiter = accountNumber.IndexOf('-'); var secondDelimiter = accountNumber.LastIndexOf('-'); if (firstDelimiter == -1 || secondDelimiter == -1) throw new ArgumentException(); var firstPart = accountNumber.Substring(0, firstDelimiter); if (firstPart.Length != startingPartLength) return false; var tempPart = accountNumber.Remove(0, startingPartLength + 1); var middlePart = tempPart.Substring(0, tempPart.IndexOf('-')); if (middlePart.Length != middlePartLength) return false; var lastPart = accountNumber.Substring(secondDelimiter + 1); if (lastPart.Length != lastPartLength) return false; return true; } }
So, we want to ensure that the AccountNumber
consists of three parts with different lengths (3, 10, and 2). Also, we want to ensure that those parts are divided by the minus sign separator.
That said, we can see that if delimiters are invalid we are throwing an exception. If any of the AccountNumber
parts is invalid, we return false. Finally, if everything goes well, we return true.
At first glance, this looks great and our validations are up to the task. But, let’s test those validation rules and make sure that everything works as expected.
Just before we test this code, we can use this validation method in the EmployeesController
:
public class EmployeesController : Controller { private readonly IEmployeeRepository _repo; private readonly AccountNumberValidation _validation; public EmployeesController(IEmployeeRepository repo) { _repo = repo; _validation = new AccountNumberValidation(); } ... [HttpPost] [ValidateAntiForgeryToken] public IActionResult Create([Bind("Name,AccountNumber,Age")] Employee employee) { if(!ModelState.IsValid) { return View(employee); } if (!_validation.IsValid(employee.AccountNumber)) { ModelState.AddModelError("AccountNumber", "Account Number is invalid"); return View(employee); } _repo.CreateEmployee(employee); return RedirectToAction(nameof(Index)); } ... }
Adding the xUnit Testing Project
Let’s start by creating a new xUnit Test Project and naming it EmployeesApp.Tests
:
A new project will prepare a single test class for use, named UnitTest1.cs
and will have installed xUnit library and xUnit runner as well.
We can remove UnitTest1
class, add a new folder Validation
and create a new class AccountNumberValidationTests
inside it.
Since we want to test the validation logic from the main project, we have to add its reference to the testing.
After these preparations, we are ready to write some tests.
If you like, you can add a new test project from the command window as well. All you have to do is to open your cmd
window next to the main project’s solution file and type:
mkdir EmployeesApp.Tests
– to create a new folder
cd EmployeesApp.Tests
– to navigate to the new folder
dotnet new xUnit
– to create the xUnit project with the same name as the parent folder
How Should We Write Unit Test Cases
When writing unit test cases, they should be well organized and easy to maintain. It is a best practice to write unit tests for small functionality.
Additionally, we shouldn’t write unit tests that depend on other unit tests. This means that we should be able to run our unit tests in any order we need. If this is not the case, we should modify our tests.
We should follow the naming test convention for each unit test we write. We are going to see this in practice in a minute.
Our unit tests have to be fast. To make them fast, they have to be as simple as possible, without dependency on other tests, and we should mock external dependencies (this is something you can read more about in our next article).
The tests that we write should be deterministic – meaning the tests should always behave the same if no changes were made to the code under the test. Let’s imagine that we write a test for a single method and that test passes. That test is deterministic if it passes every time we run it. It applies the same if we modify the code under the test and the test fails. It should fail every time we run it until the code under the test is fixed.
Unit Testing with xUnit
To start with unit testing, let’s modify the AccountNumberValidationTests
class:
public class AccountNumberValidationTests { private readonly AccountNumberValidation _validation; public AccountNumberValidationTests() => _validation = new AccountNumberValidation(); [Fact] public void IsValid_ValidAccountNumber_ReturnsTrue() => Assert.True(_validation.IsValid("123-4543234576-23")); }
We are going to use the _validation
object with all the test methods in this class. Therefore, the best way is to create it in a constructor, and then just use it when we need it. By doing so, we prevent the repetition of instantiating the _validation
object.
Below the constructor, we can see our first test method decorated with the [Fact]
attribute. Pay attention to the naming convention we use for test methods:
[MethodWeTest_StateUnderTest_ExpectedBehavior]
The method’s name implies that we are testing a valid account number and that the test method should return true. We can achieve that by using the Assert
class and the True
method which verifies that the expression inside it returns true. For the expression, we call the IsValid
method from the AccountNumberValidation
class and pass a valid account number.
Now we can run the Test Explorer and verify if our test passes:
Works great. Let’s move on.
Theory and InlineData
In the AccountNumberValidation
class, the IsValid
method contains validations for the first, middle, and last part of the account number. Therefore, we are going to write tests for all these situations. Let’s start with the test where the first part is wrong:
[Fact] public void IsValid_AccountNumberFirstPartWrong_ReturnsFalse() => Assert.False(_validation.IsValid("1234-3454565676-23"));
We expect our test to return false if we have a wrong account number. Therefore we are using the False()
method with the provided expression.
Once we run the test, we can see that it passes. But now, if we want to test an account number with 2 digits for the first part (we tested just with 4 digits), we would have to write the same method again just with a different account number. Obviously, this is not the best scenario. To improve that, we are going to modify this test method by removing the [Fact]
attribute and adding the [Theory]
and [InlineData]
attributes:
[Theory] [InlineData("1234-3454565676-23")] [InlineData("12-3454565676-23")] public void IsValid_AccountNumberFirstPartWrong_ReturnsFalse(string accountNumber) => Assert.False(_validation.IsValid(accountNumber));
By using the [Theory]
attribute, we are stating that we are going to provide some data to this test method as a parameter. Additionally, with the [InlineData]
attribute, we are providing concrete data for the test method.
Now, let’s check the result:
Even though we have only two test methods, the test runner runs three tests. One test for the first test method and two tests for each [InlineData]
attribute.
Additional Unit Tests with xUnit
Now when we know how to use the [Theory]
and [InlineData]
attributes, let’s write additional tests for our account number:
[Theory] [InlineData("123-345456567-23")] [InlineData("123-345456567633-23")] public void IsValid_AccountNumberMiddlePartWrong_ReturnsFalse(string accNumber) => Assert.False(_validation.IsValid(accNumber)); [Theory] [InlineData("123-3434545656-2")] [InlineData("123-3454565676-233")] public void IsValid_AccountNumberLastPartWrong_ReturnsFalse(string accNumber) => Assert.False(_validation.IsValid(accNumber));
There is nothing new in the code above (except different parameters). So, we can run the test runner right away and verify that everything works as expected.
Excellent! One more test to go.
Testing Exceptions with xUnit
In the IsValid
method, we verify that both delimiters should be minus signs. If this is not the case, we throw an exception. So, let’s write a test for that:
[Theory] [InlineData("123-345456567633=23")] [InlineData("123+345456567633-23")] [InlineData("123+345456567633=23")] public void IsValid_InvalidDelimiters_ThrowsArgumentException(string accNumber) => Assert.Throws<ArgumentException>(() => _validation.IsValid(accNumber));
We test three different situations here when the second delimiter is wrong, when the first delimiter is wrong, and when both delimiters are wrong. To test an exception, we have to use the Throws<T>
method with the exception type as a T value. Note that, we are using a lambda expression inside the Throws
method which is a little different from what we have used before.
Having done this, let’s check the result:
Well, would you look at that! Our test has failed.
To be more precise, two of them have failed and one has passed. So this means that our validation check in the IsValid
method is wrong. And now, we see why tests are so important. Even though the code looked like a good one at first glance, we can see that it is not that good. So, let’s fix it:
var firstDelimiter = accountNumber.IndexOf('-'); var secondDelimiter = accountNumber.LastIndexOf('-'); if (firstDelimiter == -1 || (firstDelimiter == secondDelimiter)) throw new ArgumentException();
Great!
Now, once we run the test again, it will pass.
Conclusion
So, this brings us to the end of the first article in the series.
We have learned how to create the xUnit project and how to use [Fact], [Theory], and [InlineData] attributes. Also, we have created several tests to test our validation logic from the AccountNumberValidation class. But this is just the beginning.
In the next article, we are going to learn how to test our controller class and how to use mocked objects with the testing code.