Better assertions for your unit tests

This article is part of the C# Advent calendar. During the event, two new articles are posted each day by authors throughout the community.

Introduction

Unlike many other developers, I do like unit tests. These tests have saved my professional life more than once. Without them, I would have pushed very bad mistakes to the remote codebase. As much as I like unit tests, I don't like the way assertions work in the unit testing frameworks in both xUnit and MSTest. In my opinion, the assertions do not express clearly what I want to assert and I also have to explain the correct order of arguments for Assert to new developers over and over again. For a couple of years, I started using another component for this: Fluent Assertions. In this blog post, I will show you how you can create more readable assertions using this package.

Installation

In order the start using the package, you have to install it from the NuGet-repository. 

If you want to use Fluent Assertions with ASP.NET Core you will need this package as well.

Example

I always like to use examples to explain the use of code. And since this is a blog for the C# Advent of Code, I want to stay in the Christmas spirit. A family can have a number of children. Each child can be given a gift, but the total value of all the gifts should not exceed the budget for the family. For a gift, the time to produce the gift by the Elves is recorded. If the gift is ordered when it cannot be produced anymore by the Elves, before Christmas Eve, the gift cannot be added. The domain classes for this example are shown below.

Santa asked us to write the system for this example. Since a lot of children depend on our system, we are not permitted to make mistakes in our code, so we want to write enough unit tests to ensure the correct behavior.

Based on this description I have set up a solution in .NET 7. The code and the corresponding unit tests are used to explain the use of different assertion options in Fluent Assertions.

Introduction and booleans

I start the explanation of the code with the Child class. This is a fairly simple class without much logic, but it holds enough logic to explain some basic assertions that you can use.

The Child class holds the current age for the child as a property. It is of course not best practice to include an age that can easily be calculated in a class, but in this blog post, I want to focus on the use of fluent assertions and not on all the intricacies of a correct age calculation.

There are two methods that are interesting enough to include in a unit test. The HasGift method simply returns if Gift has been assigned an object. Using the normal Assert, this can be tested with the following unit test.


The assert does not clearly express the intent I want to express in my assertion. Using Fluent Assertions, this test can be written as follows. 

The fluent syntax makes the intent more readable because it resembles normal English used in writing code.  For this simple example, the difference is not that great, but I will show you other assertions that benefit more from the fluent syntax.

Every assertion for Fluent Assertions should begin with the call to Should(). For many types, there is a separate overload for Should to include the available assertions.

Null checks

Of course, I can also check if the Gift property has not been given a value. This can be included in a unit test like the code below.

The assertions can thus check on nullable behavior. In the example, the BeNull-check is used. For most assertions, the inverted assertion, so in this case NotBeNull, is available as well.

Strings

Fluent Assertions also support assertions on strings. The following example checks if the Name has been assigned a value. I normally would not include this kind of unit test in my production code, since it holds little value. But it does show how to work with strings.

In a lot of unit tests, I also want to be able to check the format of a string. The ToString method concatenates the capitalized name and age of a child. The format can be checked in a number of ways. The easiest way is simply to check the different parts of the string.
In this example, I check if a string starts with the name and ends with the age. I also check if the hyphen is included only once in the string. All these checks can be included in one assert by using the And-method. Writing this test using the regular assertion method would require a number of different assertions. 

I personally can understand the intent of the fluent way of assertions way better than the 4 different assertions below each other. 

The contents of the string can be checked using a regular expression as well. For the demo, I kept the expression very simple to only check for a capital I at the start of the string and 10 at the end.

Enum

For checking the value of an enum, a separate overload of Should is available.

DateTime

The support for DateTime checks is demonstrated by testing the Gift class shown below. The time to produce a gift is stored in the class and based on this duration the date at which the gift will be ready, is returned by the ReadyAt property.

The only real method in this class that is worth unit testing is the ReadyAt method. When testing this method, the calculated date can be checked.

The BeExactly method works with ticks, so dates have to be converted to ticks first before they can be compared. Next to the comparison with the exact date, the date can also be checked if it is in the future.

A lot of other comparison options for DateTimes are available as well.

Collections

In many unit tests, you have to work with collections. In our example, the Family class holds a list of children that I can use to show support for collections.


Two methods deal with the collection of children: AddChild and Children. The Family allows children to be added and the enumeration of children to be retrieved. The use of yield in this example is on purpose, simply to show you that this code construct is supported as well.
The list of children of the family is kept private within the class, so I must use the Children-method to check if children have been added through the AddChild-method. Of course, I could use the option of InternalsVisibleTo, but I normally try to avoid this. 

The first thing that can be tested is that after adding a child, the children's collection is not empty.

I do not need methods like Any or Count for this check, which makes the test easier to understand.

But if I test that after adding a child, the collection is not empty anymore, I must ensure that the collection starts out with an empty list. 

For checking the number of elements in a collection, the HaveCount method can be used.

If I don't know the exact number of elements in a collection, but I do know that the count should be greater than a certain number, the HaveCountGreaterThan can be used.

All the previous checks are not that complicated. Fluent Assertions also allow you to check if a certain element is present in a collection.  And I can even check if there is only one element that matches the search. The intent of this check is in my opinion far better than using a separate LINQ query to fetch the element and then assert the existence of this element.

Exceptions

To demonstrate the handling of exceptions, I added a method for adding a gift to a child. A number of different checks have to be done before the gift can be assigned to the child. Of course, the child must be part of the family and the budget must not be exceeded. 

In our unit tests, I want to test the happy flow first.
In the unit tests to check for adding a gift to a non-existing child, I have to catch the ArgumentException. One way to do this is by calling Invoke on the subject under test and passing the method to test as a lambda. This works but it does break the Arrange, Act, and Assert flow many developers like to use.
In order to keep the Arrange, Act and Assert order in the unit test, you can use another construction shown in the code below. The method to test is captured in an action and this action is then executed during the assert phase. 

Floating point numbers

In unit tests, we often have to check the outcome of a calculation. In the Christmas example, I simulated this by adding a method to calculate the average amount for gifts within a family.
In a unit test, I can check if the average has been calculated correctly, by checking if the outcome is a positive number or by comparing the calculated value with the expected (precalculated) value. You should be very careful comparing two floating point numbers to each other. The precision of a number can result in false positive or false negative tests. To circumvent this, a number can be compared by taking precision into account by using the method BeApproximately. All these options are shown in the code below.



Conclusion

In this blog post, I showed you how you can use Fluent Assertions to improve the readability of your unit tests. Working with assertions for booleans, nullable objects, strings, dates, collections, and numbers are shown in the example code. There is a lot more that you can do with Fluent Assertions, but I hope this blog post gives you enough information to at least get started.

The code from this blog can be found here

Happy Holidays!

Comments

Popular posts from this blog

Writing unit tests with GitHub Copilot

Deploying multiple projects from one solution with Azure DevOps

Using pictures in ASP.NET MVC Core with Entity Framework Core