blog
Ottorino Bruni  

Why FluentAssertions is Essential for xUnit Testing in .NET Projects Using C# in VSCode

What is FluentAssertions?

FluentAssertions is a library that provides a rich set of extension methods to assert the outcomes of .NET tests in a more expressive and readable way. By using FluentAssertions, you can write unit tests in a fluent, human-readable style, making it easier to specify the expected behavior of your code. This approach fits perfectly into Test-Driven Development (TDD) and Behavior-Driven Development (BDD) practices, where clear and understandable test cases are essential.

In the previous post, we discussed How to Use xUnit for Unit Testing in .NET Projects Using C# in VSCode, highlighting the importance of unit testing in software development.

https://github.com/fluentassertions/fluentassertions

Now, let’s talk about why it’s essential and how it improves your xUnit tests.

Why Use FluentAssertions in xUnit?

When writing unit tests with xUnit, or any other framework (like NUnit or MSTest), you rely on assertions to verify that your code behaves as expected. However, basic assertion methods like Assert.Equal or Assert.True are often limited in terms of expressiveness, making tests harder to read and understand at first glance.

FluentAssertions solves these issues by providing the following advantages:

Improved Readability:
FluentAssertions lets you write test assertions in a way that resembles natural language. For example, instead of writing Assert.Equal(expected, actual), you can write actual.Should().Be(expected). This small shift in syntax makes your tests more readable and easier to maintain.

// xUnit traditional assertion
Assert.Equal(5, result);

// FluentAssertions alternative
result.Should().Be(5);

More Descriptive Error Messages:
When an assertion fails, FluentAssertions provides highly descriptive error messages, telling you exactly why the test failed and how the actual result differed from the expected result. This can save you time when debugging, as you won’t need to dig deep into test logs. For me, this is a key point: getting errors with better descriptions makes it easier to write and manage tests. It’s not merely about finding bugs; it’s about understanding them at a glance, which in turn, fosters a more productive and less frustrating testing cycle.

Fluent API for Complex Assertions:
FluentAssertions shines when it comes to complex assertions, such as checking collections, throwing exceptions, or deeply nested properties. For instance, instead of just asserting that an exception was thrown, you can verify both the exception type and message in a single fluent chain.

// Checking exception types and messages
Action act = () => SomeMethodThatThrows();
act.Should().Throw<ArgumentException>()
    .WithMessage("Expected error message");

Support for Multiple Frameworks:
FluentAssertions can be used with xUnit, NUnit, and MSTest. This means if your project shifts between testing frameworks, your assertions will remain consistent. It also allows you to maintain a unified assertion syntax across different teams or projects.

Customization and Extensibility:
If your project has specific domain objects or conditions that need custom assertions, FluentAssertions allows you to easily extend its functionality with your own custom extension methods. This flexibility ensures that your tests align perfectly with your project’s needs.

The Debate on Multiple Assertions in Unit Testing

For a long time, there has been a common belief in the development community that unit tests should only have one assertion. This idea has often been attributed to early guidance from prominent figures in software testing. One of the key sources of this advice is Roy Osherove, who discussed this principle in his well-known book The Art of Unit Testing.

However, Osherove later revisited this topic and clarified his stance in an article titled Avoid Multiple Asserts in a Single Unit Test (Revisited). In this article, he highlights that the original advice was often misinterpreted. The goal was not to enforce a strict “one assertion per test” rule, but to encourage tests that focus on one behavior or logical responsibility. He explains that while multiple assertions can complicate tests, they are acceptable and even recommended when they are verifying different aspects of the same behavior.

Modern Considerations:

  • Test Frameworks: Many modern testing frameworks now allow tests to continue executing even after an assertion has failed, collecting all violations. This alleviates some of the traditional issues associated with multiple assertions.
  • Development Practices: The use of multiple assertions in a single test is acceptable if:
    • The assertions are logically related and are testing closely associated aspects of the same behavior.
    • The test remains clear and readable, maintaining high maintainability.
    • An approach is employed where the test framework supports detailed diagnostics of failures.

Example: How to Use FluentAssertions with xUnit for Unit Testing

Disclaimer: This example is purely for educational purposes. There are better ways to write code and applications that can optimize this example. Use this as a starting point for learning, but always strive to follow best practices and improve your implementation.

GitHub Repository

All the code is available in this GitHub repository and use the fluentassertions branch . This repository will help you easily apply the new modifications to the previously created code thanks to the use of tags and branches.

Prerequisites

Before starting, make sure you have the following installed:

  • .NET SDK: Download and install the .NET SDK if you haven’t already.
  • Visual Studio Code (VSCode): Install Visual Studio Code for a lightweight code editor.
  • C# Extension for VSCode: Install the C# extension for VSCode to enable C# support.

You can install Fluent Assertions through the NuGet Package Manager:

Install-Package FluentAssertions
Install-Package FluentAssertions

The UserAccount class remains the same and we only need to modify the UserAccountTests class. Now, let’s rewrite this using FluentAssertions:

  1. Convert the Assert.Equal assertions into a more readable syntax.
  2. Improve the Assert.Throws assertions by checking both the exception type and its message fluently.

Here’s how the updated code would look with FluentAssertions:

using FluentAssertions;

namespace UnitTestConsoleApp.Tests;

public class UserAccountTests
{
    private UserAccount _userAccount;

    public UserAccountTests()
    {
        _userAccount = new UserAccount();
    }

    [Fact]
    public void SetEmail_ValidEmail_EmailIsSet()
    {
        // Arrange
        string email = "test@example.com";

        // Act
        _userAccount.SetEmail(email);

        // Assert
        _userAccount.GetEmail().Should().Be(email);
    }

    [Fact]
    public void SetEmail_EmptyEmail_ThrowsArgumentException()
    {
        // Arrange
        string emptyEmail = "";

        // Act and Assert
        Action act = () => _userAccount.SetEmail(emptyEmail);
        act.Should().Throw<ArgumentException>()
                .WithMessage("Email cannot be null or empty");
    }

    [Fact]
    public void GetEmail_NoEmailSet_ReturnsNull()
    {
        // Act
        string email = _userAccount.GetEmail();

        // Assert
        email.Should().BeNull();
    }

    [Theory]
    [InlineData("user@example.com")]
    [InlineData("admin@domain.com")]
    [InlineData("support@company.org")]
    public void SetEmails_ValidEmail_EmailIsSet(string email)
    {
        // Act
        _userAccount.SetEmail(email);

        // Assert
        _userAccount.GetEmail().Should().Be(email);
    }

    [Theory]
    [InlineData("")]
    [InlineData(null)]
    [InlineData(" ")]
    public void SetEmail_InvalidEmail_ThrowsArgumentException(string invalidEmail)
    {
        // Act
        Action act = () => _userAccount.SetEmail(invalidEmail);

        // Assert using FluentAssertions
        act.Should().Throw<ArgumentException>()
        .WithMessage("Email cannot be null or empty");
    }
}
Example: How to Use FluentAssertions with xUnit for Unit Testing

Key Benefits of FluentAssertions:

Readable Syntax:
Instead of Assert.Equal(expected, actual), you now write actual.Should().Be(expected). This syntax is more intuitive and reads like natural language, improving the readability of the tests.

_userAccount.GetEmail().Should().Be(email);

Better Exception Testing

When testing for exceptions, FluentAssertions lets you verify both the exception type and the message in a single fluent chain. This removes the need for separate Assert.Throws and Assert.Equal statements, making the test shorter and more expressive.

act.Should().Throw<ArgumentException>()
    .WithMessage("Email cannot be null or empty");

Clearer Failure Messages

FluentAssertions provides descriptive error messages when a test fails, showing exactly what was expected and what was received. For instance, if an assertion fails, it tells you the actual and expected values in a clear and understandable format.

Rull all tests with fluentassertions

Additional Features of FluentAssertions: Why It’s a Game Changer

Beyond basic assertions, FluentAssertions provides powerful and concise ways to assert more complex scenarios in your unit tests. Let’s dive into some key features like Assertion Chaining, Collection Assertions, Exception Assertions, Object Comparison, and Time Assertions.

Assertion Chaining

One of the most useful features of FluentAssertions is assertion chaining. You can combine multiple assertions into a single line, which not only condenses your tests but also makes them more readable.

// Before (xUnit)
Assert.NotNull(result);
Assert.IsType<ServiceResult>(result);

// After (FluentAssertions)
result.Should().NotBeNull().And.BeOfType<ServiceResult>();

Collection Assertions

Working with collections in unit tests can be tedious, especially if you need to check the number of items, their order, and their contents. FluentAssertions makes these operations straightforward and elegant.

// Before (xUnit)
Assert.Equal(3, items.Count);
Assert.Equal("Item1", items[0]);
Assert.Equal("Item2", items[1]);
Assert.Equal("Item3", items[2]);

// After (FluentAssertions)
items.Should().HaveCount(3).And.ContainInOrder("Item1", "Item2", "Item3");

Prettier Exception Assertions

FluentAssertions also makes exception assertions more elegant by eliminating the need for temporary variable assignments. You can chain the checks for the exception type and message.

// Before (xUnit)
var exception = Assert.Throws<InvalidCarMakeException>(action);
Assert.Equal("Invalid car make: InvalidMake", exception.Message);

// After (FluentAssertions)
action.Should()
    .ThrowExactly<InvalidCarMakeException>()
    .WithMessage("Invalid car make: InvalidMake");

Object Comparison

When comparing objects in tests, using traditional assertions requires you to compare individual fields manually. FluentAssertions simplifies this by offering deep comparison between objects with a single method.

// Before (xUnit)
Assert.Equal(expected.Id, actual.Id);
Assert.Equal(expected.Name, actual.Name);
Assert.Equal(expected.Description, actual.Description);

// After (FluentAssertions)
actual.Should().BeEquivalentTo(expected);

Time Assertions

Comparing DateTime values in unit tests can be tricky because external services or operations often introduce small time variations. FluentAssertions offers a more precise and readable way to compare time values.

// Before (xUnit)
Assert.True((DateTime.Now - actual).TotalSeconds < 1);

// After (FluentAssertions)
actual.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(1000));

Custom Assertions in FluentAssertions

When writing unit tests, you might encounter cases where you need to perform the same set of assertions on a specific type multiple times. FluentAssertions allows you to create custom assertions that encapsulate complex or domain-specific logic, which you can reuse across your test suite.

public static class PersonAssertions
{
    [CustomAssertion]
    public static void NotHaveLastNameDoe(this Person person)
    {
        // Ensure the person object is not null
        Execute.Assertion.ForCondition(person != null)
            .FailWith("Expected person not to be null.");

        // Ensure the person's last name is not "Doe"
        Execute.Assertion.ForCondition(!string.Equals(person.LastName, "Doe", StringComparison.OrdinalIgnoreCase))
            .FailWith("Expected person's last name not to be 'Doe'.");
    }
}
[Fact]
public void Person_Should_Not_Have_LastName_Doe()
{
    // Arrange
    var person = new Person { FirstName = "John", LastName = "Smith" };

    // Act and Assert using FluentAssertions and custom assertion
    person.Should().NotHaveLastNameDoe();
}

Conclusion

Writing effective and meaningful unit tests is essential for ensuring the reliability and maintainability of your code, regardless of the testing framework you use. Whether you’re using xUnit, NUnit, or MSTest, clear and expressive tests play a pivotal role in identifying and fixing bugs early in the development process.

FluentAssertions enhances your testing experience by providing more readable, expressive, and flexible assertions. Through improved syntax, detailed failure messages, and the ability to chain assertions, FluentAssertions makes your tests more intuitive and easier to maintain. This not only reduces the effort required to write tests but also makes them more enjoyable to work with, whether you’re debugging or writing new features.

The advantages FluentAssertions brings to the table include:

  • Readable Syntax: Writing assertions in a fluent, natural language style.
  • Detailed Error Messages: Clear, descriptive error messages that make debugging faster and easier.
  • Complex Assertion Handling: Handling collections, exceptions, and deeply nested objects effortlessly.
  • Extensibility: Creating custom assertions tailored to your project’s needs.
  • Framework Agnostic: Works seamlessly with multiple testing frameworks, including xUnit, NUnit, and MSTest.

Incorporating FluentAssertions into your xUnit tests in .NET projects using C# and VSCode not only improves the quality of your test suite but also helps you embrace modern testing practices with minimal overhead. It transforms the way you write, understand, and maintain tests, making it an essential tool for any .NET developer aiming to build robust, high-quality applications.

If you think your friends or network would find this article useful, please consider sharing it with them. Your support is greatly appreciated.

Thanks for reading!

Leave A Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.