In this post, we’ll continue to look at xUnit with a few more simple examples. If you haven’t checked out the previous posts in this series, you can do so with the links, below:
- Part 1: An Introduction to Automated Testing
- Part 2: Frameworks & Tools for Automated Testing in the .NET Ecosystem
- Part 3: Your First Unit Test in xUnit
TodoListManager Class
Let’s look at another simple C# class I’ve written for this demo. It’s a simple Todo List class with a single AddTodo method that allows you to add a Todo item to a list.
using System.Collections.Generic; namespace UnitTestsDemoPart2 { public class TodoListManager { public List<string> TodoList { get; set; } public void AddTodo(string task) { TodoList.Add(task); } } }
But does this code work? Hint: It doesn’t. The code will certainly compile without any errors giving you the false impression that things may be okay but it is going to throw a runtime error. Let’s write a unit-test to check and see if this code runs:
using Xunit; namespace UnitTestsDemoPart2.UnitTests { public class TodoListManagerTests { [Fact] public void AddTodo_WhenCalledWithATodoItem_AddsItemToList() { // Arrange var sut = new TodoListManager(); // Act sut.AddTodo("Get milk"); // Assert Assert.Single(sut.TodoList); } } }
A couple of things to call out, regarding my unit test:
- You’ll notice that I’m using the three-part naming convention that I introduced in the previous post, for my test method. I’m also using the Arrange -> Act -> Assert pattern. If you are unfamiliar, please review my previous post.
- Under the arrange part, I named the object that I’m testing as sut. This is another common convention that you’ll see in unit and other tests in the wild. It’s an acronym that stands for System Under Test.
- I introduce another Assertion method that xUnit provides, Assert.Single. This method checks to see that there is a single item in the collection that you are passing it and returns that lone item.
Now, let’s run this test and you’ll see that it fails.
You’ll see that the code encountered a null-reference exception causing the unit-test to fail. We had forgotten to initialize the TodoList List<string> variable before trying to add an item into this list. Although this is a simple, contrived example, you can probably imagine the peace of mind that you can get with having a robust set of unit-tests with a great code-coverage that will help you catch issues before they are promoted into production.
Here’s a modified version of the TodoListManager class with the null-reference exception handled:
using System.Collections.Generic; namespace UnitTestsDemoPart2 { public class TodoListManager { public List<string> TodoList { get; set; } = new List<string>(); public void AddTodo(string task) { TodoList.Add(task); } } }
Now let’s add another assertion to our test. While we previously tested to see if there is a single item in the Todo List, let’s write another explicit assertion to confirm that the single item in there is indeed the item that we are passing it – “Get milk”.
using Xunit; namespace UnitTestsDemoPart2.UnitTests { public class TodoListManagerTests { [Fact] public void AddTodo_WhenCalledWithATodoItem_AddsItemToList() { // Arrange var sut = new TodoListManager(); // Act sut.AddTodo("Get milk"); // Assert var result = Assert.Single(sut.TodoList); Assert.Equal("Get milk", result); } } }
Unit Tests => Clean Code => Unit Tests =>
You can add as many Asserts as you need to a single test to confirm various things. But in general, try to keep it to a minimum. If you find yourself writing many, many asserts, it’s probably a sign that you may have to either break those asserts up into multiple tests or perhaps even refactor the class or method that you are testing into multiple methods or classes to ensure that no one thing has many responsibilities. While we won’t go into SOLID design principles (you can google it for now) in this post, just keep in mind that a good side-effect of writing unit tests is that it will help you in writing better code.
Fact vs Theory in xUnit
Let’s look at another example class to learn about Fact vs Theory in xUnit. Below, I have a very simple Calculator class. It has a single method GetTheSumOfTwoNumbers that adds up two integers and returns the result.
namespace UnitTestsDemoPart2 { public class Calculator { public int GetTheSumOfTwoNumbers(int a, int b) { return a + b; } } }
Let’s write a few unit tests against this class and method. You’ll want to test and make sure that it sums up two numbers and provides the expected result. You may also want to verify that when you pass a zero as one of the parameters, it still works as expected. How about when you pass it two negative numbers? How about when you pass it the maximum integer value possible? Your unit test may start looking like this:
using Xunit; namespace UnitTestsDemoPart2.UnitTests { public class CalculatorTests { [Fact] public void GetTheSumOfTwoNumbers_WhenCalledWithTwoNumbers_ReturnsTheSum() { var sut = new Calculator(); var result = sut.GetTheSumOfTwoNumbers(1, 2); Assert.Equal(3, result); } [Fact] public void GetTheSumOfTwoNumbers_WhenCalledWithTwoNegativeNumbers_ReturnsTheSum() { var sut = new Calculator(); var result = sut.GetTheSumOfTwoNumbers(-1, -2); Assert.Equal(-3, result); } [Fact] public void GetTheSumOfTwoNumbers_WhenCalledWithZeroAndAnotherNumber_ReturnsTheSum() { var sut = new Calculator(); var originalNumber = 1; var result = sut.GetTheSumOfTwoNumbers(0, originalNumber); Assert.Equal(originalNumber, result); } } }
You may see a growing problem with this. You have a lot of duplicate code here with very little variance among them – only our test data is changing while everything else remains the same. xUnit has a nice construct called Theory that addresses this very issue. It allows you to construct a single test and pass it multiple sets of test data. The framework will execute each payload as a different test and report on it accordingly. Here’s the above example rewritten using the Theory attribute:
using Xunit; namespace UnitTestsDemoPart2.UnitTests { public class CalculatorTestsWithTheory { [Theory] [InlineData(1, 2, 3)] [InlineData(-1, -2, -3)] [InlineData(0, 1, 1)] public void GetTheSumOfTwoNumbers_WhenCalledWithTwoNumbers_ReturnsTheSum(int a, int b, int expectedResult) { var sut = new Calculator(); var result = sut.GetTheSumOfTwoNumbers(a, b); Assert.Equal(expectedResult, result); } } }
They are treated as separate tests and shows up as such in the Test Explorer:
Parting Thoughts
Today, we got a chance to look at some of the basic mechanics of writing unit tests in xUnit. In the next episode, we’ll take a step back and look at some of the types of things you can test for in your operational code.
If you want to play with any of the examples that I featured in this post, please feel free to download the companion repository for this post from my GitHub account, here: