Enhance your .NET Testing #6: The Art of Assertion with FluentAssertions

FluentAssertions is the Swiss Army knife of assertion libraries for .NET developers. If you want to supercharge your tests to make them more expressive, readable, and robust, this is the tool to have in your arsenal.

In this article, we'll dive deep into the FluentAssertions library, exploring its extensive capabilities and demonstrating how it can simplify your unit testing workflow.

If you didn't read the previous article, I recommend you to do it:

Enhance your .NET Testing #5: Integration tests with Docker.DotNet
This article delves deep into the intricacies of Docker.DotNet, guiding you through the creation of integration tests using this remarkable library.

Unpacking FluentAssertions

Developed by Dennis Doomen and first released in 2011, FluentAssertions has since become a beloved tool among .NET developers. Its primary goal is to make your test assertions more expressive, readable, and robust.

It achieves this by providing a fluent and natural syntax that allows you to write human-readable assertions. It's more than just an assertion library; it's a bridge to more intuitive and descriptive testing, finding its application in both simple unit tests and complex integration tests.

To begin using FluentAssertions, you need to install the FluentAssertions NuGet package. Once installed, you can start writing assertions in a more intuitive and expressive manner. Let's walk through a quick example to get you started.

Consider a scenario where you want to assert that a string variable actualString is equal to the expected string "Hello, World!". In traditional unit testing frameworks, you might write an assertion like this:

Assert.AreEqual("Hello, World!", actualString);

While this assertion gets the job done, it lacks readability and expressiveness. Now, let's see how FluentAssertions can improve this:

actualString.Should().Be("Hello, World!");

With FluentAssertions, the assertion reads like plain English: "actualString should be 'Hello, World!'" – clear, concise, and easy to understand.

As Jonathan Roellinger recommended, FluentAssertions also lets you add error messages related to validations, specifying the reason for the error.

var stringCreator = new StringCreator();
var result = stringCreator.Build("Hello", "World");

// Assert
result.Should().Be("Hello, World!")
      .BecauseArgs("Hello", "World") // Using BecauseArgs to specify argument values
      .Because("We expect the stringCreator to correctly build the hello world string."); // Providing context

A Deeper Dive into FluentAssertions

Now that we've seen a basic example let's explore some of the most commonly used assertion methods provided by FluentAssertions across various data types:

Generic Assertions

These assertions are versatile and allow you to perform equality checks and null checks with ease. They can be used on all types of objects. Here's how they work:

string? stringValue = "FluentAssertions";

stringValue.Should().Be("FluentAssertions"); // Should be equal to the value
stringValue.Should().NotBe("OtherString");   // Should be different from the value
stringValue.Should().BeNull();               // Should be null
stringValue.Should().NotBeNull();            // Should not be null

String Assertions

Moving on to String Assertions to focus on validating and asserting string-related conditions. These assertions simplify common string checks:

string actualString = "FluentAssertions";

actualString.Should().Contain("Assertions");  // Should contain the specified substring
actualString.Should().StartWith("Fluent");    // Should start with the specified prefix
actualString.Should().EndWith("Fluent");      // Should end with the specified suffix
actualString.Should().MatchRegex("Fluent.*"); // Should match the specified regex pattern

Integer Assertions

Integer Assertions allow to perform checks on integer values, ensuring they meet specific criteria on the value:

int actualValue = 42;

actualValue.Should().BeGreaterThan(30); // Should be greater than the specified value
actualValue.Should().BeInRange(1, 100); // Should be within the specified range
actualValue.Should().BePositive();      // Should be positive
actualValue.Should().BeNegative();      // Should be negative

Boolean Assertions

For Boolean Assertions, it mainly check if the value is true or false:

bool isTrue = true;

isTrue.Should().BeTrue();  // Should be true
isTrue.Should().BeFalse(); // Should be false

DateTime Assertions

Next, DateTime Assertions ensure precise control of date and time conditions for easy comparison :

DateTime date1 = DateTime.Now;
DateTime date2 = date1.AddMinutes(5);

date1.Should().BeBefore(date2);                            // Should be before the specified date
date2.Should().BeAfter(date1);                             // Should be after the specified date
date1.Should().BeCloseTo(date2, TimeSpan.FromMinutes(10)); // Should be close to the specified date within the precision

List Assertions

List Assertions provide a powerful means of validating lists and collections. They are undoubtedly the most practical assertions, as they incorporate validations that are more complicated to perform with Assert, notably on the comparison of values:

var numbers = new List<int> { 1, 2, 3, 4 };

numbers.Should().Contain(3);                                   // Should contain the specified item
numbers.Should().NotContain(5);                                // Should not contain the specified item
numbers.Should().HaveCount(4);                                 // Should have the specified count of elements
numbers.Should().BeEquivalentTo(new List<int> { 4, 3, 2, 1 }); // Should be equivalent to the specified collection

Type Assertions

For Type Assertions, it mainly check if the type or the cast on a type is possible:

object obj = "FluentAssertions";

obj.Should().BeAssignableTo<string>();  // Should be assignable to the specified type
obj.Should().BeOfType<string>();        // Should be of the specified type

Exception Assertions

Exception Assertions make it very easy to catch exceptions to check that a logical part of the application is actually triggered:

Action action = () => { throw new InvalidOperationException(); };

action.Should().Throw<InvalidOperationException>();        // Should throw the specified exception type
action.Should().ThrowExactly<InvalidOperationException>(); // Should throw exactly the specified exception type
action.Should().NotThrow();                                // Should not throw any exception

Task Assertions:

Next, Task Assertions guarantee the correct execution and simplify testing asynchronous code:

async Task<int> DelayedTask()
{
    await Task.Delay(100);
    return 42;
}

await DelayedTask().Should().NotThrowAsync();          // Task should complete successfully without throwing an exception
await DelayedTask().Should().BeCompletedSuccessfully(); // Task should complete successfully without exceptions

HttpResponseMessage Assertions (for testing HTTP responses):

Finally, we look at HttpResponseMessage Assertions for testing HTTP responses when tests are combined with an HTTP call, as with the WebApplicationFactory:

var httpResponse = new HttpResponseMessage(HttpStatusCode.OK);
httpResponse.Headers.Add("Content-Type", "application/json");
httpResponse.Content = new StringContent("{\"message\":\"Hello, World!\"}");

httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);              // Should have the specified status code
httpResponse.Should().HaveHeader("Content-Type");                     // Should have the specified header
httpResponse.Should().HaveContent("{\"message\":\"Hello, World!\"}"); // Should have the specified content
💡
If you have never heard about WebApplicationFactory, we wrote an great article about it: read it now

These assertions are adaptable to a wide range of scenarios, allowing you to express your test expectations in a clear and concise manner.

Summary

By using FluentAssertions, you can write unit tests that read like plain English sentences, making them more understandable to both developers and non-developers. The expressive syntax not only enhances the quality of your tests but also simplifies the process of identifying and resolving issues.

Incorporating FluentAssertions into your testing workflow empowers you to write elegant, maintainable, and effective unit tests, ultimately leading to more robust and reliable software.

Next article:

Enhance your .NET Testing #7: 5 best practices to write better tests
In the digital age, where software permeates every facet of life, the significance of reliable code cannot be overstated. Imagine the chaos when a banking system falters, or the disruption when an e-commerce platform crashes. These scenarios underscore the critical role of testing in the software development lifecycle. Testing is

To go further:

Introduction
A very extensive set of extension methods that allow you to more naturally specify the expected outcome of a TDD or BDD-style unit tests. Targets .NET Framework 4.7, .NET Core 2.1 and 3.0, as well as .NET Standard 2.0 and 2.1.

Have a goat day 🐐