FluentAssertions Patterns for Clearer .NET Tests

The built-in Assert class in xUnit gets the job done, but its failure messages are often cryptic: "Assert.Equal() Failure. Expected: 5, Actual: 3." When a test fails, you want to know what failed and why as quickly as possible. FluentAssertions transforms your test assertions into readable statements that produce detailed failure messages.

Note on licensing: FluentAssertions 7+ moved to a commercial licence for non-open-source projects. Version 6.x remains free under the Apache 2.0 licence. If your project is commercial, evaluate the licensing terms before upgrading or consider alternatives like Shouldly.

Installation

Example.cs
dotnet add package FluentAssertions

Basic Assertions

The core pattern is simple: call .Should() on any object, then chain an assertion method.

Example.cs
// Instead of: Assert.Equal("Alice", user.Name);
user.Name.Should().Be("Alice");

// Instead of: Assert.True(user.IsActive);
user.IsActive.Should().BeTrue();

// Instead of: Assert.NotNull(result);
result.Should().NotBeNull();

// Instead of: Assert.InRange(score, 0, 100);
score.Should().BeInRange(0, 100);

When user.Name is "Bob" instead of "Alice", FluentAssertions reports: Expected user.Name to be "Alice", but found "Bob". The variable name appears in the message because FluentAssertions inspects the calling expression.

String Assertions

Example.cs
email.Should().Contain("@");
email.Should().EndWith(".com");
email.Should().MatchRegex(@"^[\w.-]+@[\w.-]+\.\w+$");
email.Should().NotBeNullOrWhiteSpace();

// Case-insensitive comparison
name.Should().BeEquivalentTo("ALICE");

// Wildcard matching
path.Should().Match("/api/users/*/orders");

Collection Assertions

This is where FluentAssertions truly shines. Collection assertions are far more expressive than what's available out of the box:

Example.cs
var users = await _repository.GetActiveUsersAsync();

users.Should().HaveCount(3);
users.Should().NotBeEmpty();
users.Should().ContainSingle(u => u.Role == "Admin");
users.Should().OnlyContain(u => u.IsActive);
users.Should().BeInAscendingOrder(u => u.Name);

// Check that a collection contains specific items
users.Select(u => u.Name)
    .Should().Contain(new[] { "Alice", "Bob" })
    .And.NotContain("Charlie");

Object Graph Comparison

Comparing complex objects field by field is a chore. BeEquivalentTo does a deep structural comparison:

Example.cs
var expected = new UserDto
{
    Name = "Alice",
    Email = "[email protected]",
    Roles = new[] { "Admin", "Editor" }
};

var actual = _mapper.Map<UserDto>(userEntity);

actual.Should().BeEquivalentTo(expected);

You can exclude specific properties or customise the comparison:

Example.cs
actual.Should().BeEquivalentTo(expected, options => options
    .Excluding(u => u.Id)
    .Excluding(u => u.CreatedAt)
    .WithStrictOrdering()); // Enforce collection ordering

This is particularly useful for testing mappers, DTOs, and API responses where you need to compare shapes without caring about auto-generated fields.

Exception Assertions

Example.cs
var act = () => _service.TransferFunds(fromAccount: "A", toAccount: "B", amount: -100);

act.Should().Throw<ArgumentException>()
    .WithMessage("*cannot be negative*")
    .WithParameterName("amount");

For async methods:

Example.cs
var act = async () => await _service.GetUserAsync(userId: 0);

await act.Should().ThrowAsync<NotFoundException>()
    .WithMessage("User with ID 0 was not found");

DateTime Assertions

Example.cs
var orderDate = order.CreatedAt;

orderDate.Should().BeAfter(DateTime.UtcNow.AddMinutes(-1));
orderDate.Should().BeBefore(DateTime.UtcNow);
orderDate.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));

BeCloseTo is essential for testing timestamps — it accounts for the small time difference between creating the object and asserting on it.

Assertion Scopes

When a test has multiple assertions, the first failure stops execution by default. AssertionScope collects all failures and reports them together:

Example.cs
[Fact]
public void UserProfile_HasExpectedValues()
{
    var profile = _service.GetProfile("alice");

    using (new AssertionScope())
    {
        profile.Name.Should().Be("Alice Smith");
        profile.Email.Should().Contain("@");
        profile.Roles.Should().Contain("Admin");
        profile.IsVerified.Should().BeTrue();
    }
}

If both Name and IsVerified are wrong, you see both failures in a single test run rather than having to fix them one at a time.

Custom Failure Messages

Add context to assertions with Because:

Example.cs
user.Should().NotBeNull("because the repository should always return a default user");
balance.Should().BePositive("because overdrafts are not permitted for this account type");

The because text appears in the failure message, making it clear why the assertion matters.

Practical Advice

FluentAssertions is one of those libraries that, once you've used it, makes going back to basic assertions feel like a step backwards. The readability gains are significant, and the failure messages alone justify the dependency.