Fluent Assertions: Fluently Assert the Result of .NET Tests
Tags: programming, coding, csharp, dotnet
Published: 2023-07-11 17:33:16

> ℹ️ **Information**
> The code in the following article was tested on .NET and ASP.NET 6 and 7.

Fluent Assertions was created to provide a more expressive way of writing assertions in .NET unit tests. Typically, assertions in unit tests can be syntactically complex and not very expressive, which makes them hard to read and understand. Fluent Assertions aims to make assertions more human-readable by using a natural language syntax.

This library extends the traditional assertions provided by frameworks like [MSTest](https://github.com/microsoft/testfx), [NUnit](https://nunit.org/), or [XUnit](https://xunit.net/) by offering a more extensive set of extension methods. Fluent Assertions supports a wide range of types like collections, strings, and objects and even allows for more advanced assertions like throwing exceptions.

## Installation

You can install Fluent Assertions through the NuGet Package Manager:

```
Install-Package FluentAssertions
```

Or using the .NET CLI from a terminal window:

```
dotnet add package FluentAssertions
```

## First assertion

Suppose you have an integer variable `result`:

```csharp
var result = 10;
```

In order to check the value of the integer variable `result`, you can write the assertion in the following way:

```csharp
result.Should().Be(expectedValue)
```

where `expectedValue` is the value you expect result to be. In this expression, the `Should()` extension method plays a key role, as it serves as the entry point for the Fluent Assertions library's functionality.

One of the primary strengths of `Should()` is its contribution to readability. By using `Should()`, the assertions begin to resemble natural language. Take for example `result.Should().Be(10)` it reads almost like an english sentence: "Result should be 10". This natural flow makes it far simpler for developers to comprehend what exactly is being tested.

Furthermore, `Should()` empowers the utilization of the fluent interface pattern. This design allows for a seamless chaining of methods, which not only keeps the code succinct, but also substantially augments the expressiveness of tests.

Additionally, the Fluent Assertions library is structured to support extensive customization and flexibility through the methods available after calling `Should()`. With a rich set of options such as `BeGreaterThan()`, `BeOfType()` and many others, it allows for crafting assertions that can efficiently address a wide range of testing scenarios.

Another significant aspect of using `Should()` is how it impacts maintenance and communication. Tests often double as documentation, providing insights into the expected behavior of code. When `Should()` is employed to craft assertions, the tests closely mirror spoken language. This closeness is invaluable as it not only makes maintenance easier but also facilitates clear communication among developers. This is particularly beneficial for those who may not have an in-depth familiarity with the codebase.

## Nullable types

For asserting whether a variable has (not) value or is (not) `null`, the following methods can be used:

```csharp
int? result = null;

result.Should().BeNull();
result.Should().HaveValue();

result.Should().NotBeNull();
result.Should().NotHaveValue();
```

Furthermore, it is possible to verify a nullable type via a predicate:

```csharp
int? result = 10;

result.Should().Match(x => !x.HasValue);
```

## Booleans

For asserting whether a variable is `true` or `false`, you can use:

```csharp
var result = true;
var otherBoolean = true;

result.Should().BeTrue();
result.Should().BeFalse();
result.Should().Be(otherBoolean);
result.Should().NotBe(otherBoolean);
```

Another interesting method for booleans is `Imply()`, which exploits the concept of boolean implication.

```csharp
var result = false;
var otherBoolean = true;

result.Should().Imply(otherBoolean);
```

Boolean implication `A implies B` simply means "if A is true, then B is also true". This implies that if A is not true, then B can be anything. The symbol used to denote `implies` is `A => B`. Thus:

| A | B | A => B |
| - | - | ------ |
| T | T | T      |
| T | F | F      |
| F | T | T      |
| F | F | T      |

`A => B` is an abbreviation for `(not A) or B`, i.e., "either A is false, or B must be true".

## Strings

For asserting whether a string is `null`, empty, or contains (not) whitespace only, you have several methods at your disposal:

```csharp
var result = "This is an example";

result.Should().BeNull();
result.Should().BeEmpty();
result.Should().HaveLength(0);
result.Should().BeNullOrWhiteSpace();

result.Should().NotBeNull();
result.Should().NotBeEmpty();
result.Should().NotBeNullOrWhiteSpace();
```

To ensure the characters in a string are all (not) upper or lower cased, you can use the following assertions:

```csharp
var result = "This is an example";

result.Should().BeUpperCased();
result.Should().BeLowerCased();

result.Should().NotBeUpperCased();
result.Should().NotBeLowerCased();
```

> ⚠️ **Warning**
> Numbers and special characters do not have casing, so `BeUpperCased` and `BeLowerCased` will always fail on a string that contains anything but alphabetic characters.

Of course, there are also other methods for string assertions:

```csharp
var result = "This is an example";

result.Should().Be("This is an example");
result.Should().BeEquivalentTo("THIS IS AN EXAMPLE");

result.Should().NotBe("This is another example");
result.Should().NotBeEquivalentTo("THIS IS ANOTHER EXAMPLE");

result.Should().BeOneOf("That is an example", "This is an example");

result.Should().Contain("is an");
result.Should().Contain("is an", AtMost.Times(5));
result.Should().ContainAll("should", "contain", "all", "of", "these");
result.Should().ContainAny("any", "of", "these", "will", "do");
result.Should().ContainEquivalentOf("THIS IS AN EXAMPLE");
result.Should().ContainEquivalentOf("THIS IS AN EXAMPLE", Exactly.Thrice());

result.Should().NotContain("is an");
result.Should().NotContainAll("can", "contain", "some", "but", "not", "all");
result.Should().NotContainAny("cannot", "contain", "any", "of", "these");
result.Should().NotContainEquivalentOf("THIS IS ANOTHER EXAMPLE");

result.Should().StartWith("This");
result.Should().StartWithEquivalentOf("THIS");

result.Should().NotStartWith("That");
result.Should().NotStartWithEquivalentOf("THAT");

result.Should().EndWith("an example");
result.Should().EndWithEquivalentOf("AN EXAMPLE");

result.Should().NotEndWith("an other example");
result.Should().NotEndWithEquivalentOf("AN OTHER EXAMPLE");
```

> 📝 Note
> All methods ending with `EquivalentOf` are case insensitive.

If you prefer a more fluent syntax than `Exactly.Once()`, `AtLeast.Twice()`, `AtMost.Times(5)` and so on, you can proceed as follows:

```csharp
var result = "This is an example";

result.Should().Contain("is an", 1.TimesExactly()); // equivalent to Exactly.Once()
result.Should().Contain("is an", 2.TimesOrMore());  // equivalent to AtLeast.Twice()
result.Should().Contain("is an", 5.TimesOrLess());  // equivalent to AtMost.Times(5)
```

In addition, the `Match`, `NotMatch`, `MatchEquivalentOf` and `NotMatchEquivalentOf` methods support the following wildcards in the pattern:

| Wildcard specifier | Matches |
| ------------------ | ------- |
| * (asterisk)       | Zero or more characters in that position. |
| ? (question mark)  | Exactly one character in that position. |

> ⚠️ **Warning**
> The wildcard pattern does not support regular expressions.

For instance, if you would like to assert that some email address is correct, use this:

```csharp
var result = "john.doe@example.com";

result.Should().Match("*@*.com");
```

## Numerics

For asserting a numeric variable, you have several methods at your disposal:

```csharp
var result = 10;

result.Should().Be(10);
result.Should().NotBe(5);
result.Should().BeGreaterThan(5);
result.Should().BeGreaterThanOrEqualTo(5);
result.Should().BeLessThan(15);
result.Should().BeLessThanOrEqualTo(15);
result.Should().BePositive();
result.Should().BeNegative();

result.Should().BeInRange(5, 15);
result.Should().NotBeInRange(11, 20);

result.Should().Match(x => x % 2 == 0);

result.Should().BeOneOf(5, 10, 15);
```

The `Should().Be()` and `Should().NotBe()` methods are not available for floats and doubles, this is because floating-point variables are inherently imprecise and should never be compared for equality. Instead, you can use `BeInRange()` or the following method:

```csharp
var result = 3.1415927F;

result.Should().BeApproximately(3.14F, 0.01F);
```

This will verify that the value of the float is between 3.139 and 3.141.

Conversely, to assert that the value differs by an amount, you can do this:

```csharp
var result = 3.5F;

result.Should().NotBeApproximately(2.5F, 0.5F);
```

This will verify that the value of the float is not between 2.0 and 3.0.

## Dates and times

For asserting a `DateTime`, several methods are provided:

```csharp
var result = new DateTime(2023, 07, 07, 12, 15, 00, DateTimeKind.Utc);

result.Should().Be(7.July(2023).At(12, 15));
result.Should().BeAfter(1.June(2023));
result.Should().BeBefore(1.August(2023));
result.Should().BeOnOrAfter(1.June(2023));
result.Should().BeOnOrBefore(1.August(2023));
result.Should().BeSameDateAs(7.July(2023).At(12, 16));

result.Should().NotBe(1.July(2023).At(12, 15));
result.Should().NotBeAfter(2.August(2023));
result.Should().NotBeBefore(1.June(2023));
result.Should().NotBeOnOrAfter(2.August(2023));
result.Should().NotBeOnOrBefore(1.June(2023));
result.Should().NotBeSameDateAs(6.July(2023));

result.Should().BeIn(DateTimeKind.Utc);

result.Should().BeOneOf(
    7.July(2023).At(12, 15),
    7.July(2023).At(13, 15),
    7.July(2023).At(14, 15)
);
```

If you only care about specific parts of a date or time, use the following assertion methods instead:

```csharp
var result = new DateTime(2023, 07, 07, 12, 15, 00, DateTimeKind.Utc);

result.Should().HaveDay(7);
result.Should().HaveMonth(7);
result.Should().HaveYear(2023);
result.Should().HaveHour(12);
result.Should().HaveMinute(15);
result.Should().HaveSecond(0);

result.Should().NotHaveDay(1);
result.Should().NotHaveMonth(6);
result.Should().NotHaveYear(2022);
result.Should().NotHaveHour(11);
result.Should().NotHaveMinute(16);
result.Should().NotHaveSecond(1);
```

To assert that a date/time is (not) within a specified time span from another date/time value you can use this method:

```csharp
result.Should().BeCloseTo(7.July(2023).At(12, 15), 2.Seconds());
result.Should().NotBeCloseTo(8.July(2023), 1.Hours());
```

This can be particularly useful if your database truncates date/time values.

## Collections

For asserting a collection, both have two possibilities:

1️⃣ **Perform assertions on the entire collection**

```csharp
IEnumerable<int> collection = new[] { 1, 2, 3, 4 };

collection.Should().BeEmpty();
collection.Should().BeNullOrEmpty();
collection.Should().NotBeNullOrEmpty();

collection.Should().Equal(new List<int> { 1, 2, 3, 4 });
collection.Should().Equal(1, 2, 3, 4);
collection.Should().BeEquivalentTo(new[] { 1, 2, 3, 4 });

collection.Should().NotEqual(new List<int> { 5, 6, 7, 8 });
collection.Should().NotBeEquivalentTo(new[] { 5, 6, 7, 8 });

collection.Should().HaveCount(4);
collection.Should().HaveCountGreaterThan(3);
collection.Should().HaveCountGreaterThanOrEqualTo(4);
collection.Should().HaveCountLessThan(5);
collection.Should().HaveCountLessThanOrEqualTo(4);
collection.Should().HaveSameCount(new[] { 6, 2, 0, 5 });

collection.Should().NotHaveCount(3);
collection.Should().NotHaveSameCount(new[] { 6, 2, 0 });

collection.Should().StartWith(1);
collection.Should().StartWith(new[] { 1, 2 });
collection.Should().EndWith(4);
collection.Should().EndWith(new[] { 3, 4 });

collection.Should().BeSubsetOf(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, });

collection.Should().ContainSingle();
collection.Should().ContainSingle(x => x > 3);

collection.Should().Contain(x => x > 3);
collection.Should().Contain(collection, "", 5, 6); // It should contain the original items, plus 5 and 6.

collection.Should().NotContain(10);
collection.Should().NotContain(new[] { 10, 11 });
collection.Should().NotContain(x => x > 10);

collection.Should().OnlyContain(x => x < 10);
collection.Should().ContainItemsAssignableTo<int>();

collection.Should().ContainInOrder(new[] { 1, 5, 8 });
collection.Should().NotContainInOrder(new[] { 5, 1, 2 });

collection.Should().ContainInConsecutiveOrder(new[] { 2, 5, 8 });
collection.Should().NotContainInConsecutiveOrder(new[] { 1, 5, 8 });

collection.Should().NotContainNulls();

IEnumerable<int> otherCollection = new[] { 1, 2, 3, 4, 1 };
IEnumerable<int> anotherCollection = new[] { 10, 20, 30, 40, 10 };
collection.Should().IntersectWith(otherCollection);
collection.Should().NotIntersectWith(anotherCollection);

const int element = 2;
const int successor = 3;
const int predecessor = 1;
collection.Should().HaveElementPreceding(successor, element);
collection.Should().HaveElementSucceeding(predecessor, element);
```

To assert that a set contains elements in a certain order, it is sufficient to use one of the following methods:

```csharp
IEnumerable<int> collection = new[] { 1, 2, 3, 4 };

collection.Should().BeInAscendingOrder();
collection.Should().BeInDescendingOrder();

collection.Should().NotBeInAscendingOrder();
collection.Should().NotBeInDescendingOrder();

collection.Should().BeInAscendingOrder(x => x.SomeProperty);
collection.Should().BeInDescendingOrder(x => x.SomeProperty);

collection.Should().NotBeInAscendingOrder(x => x.SomeProperty);
collection.Should().NotBeInDescendingOrder(x => x.SomeProperty);
```

As mentioned at the beginning of the article, multiple assertions can be concatenated with each other using the `And` property:

```csharp
IEnumerable<int> collection = new[] { 1, 2, 3, 4 };

collection.Should().NotBeEmpty()
    .And.HaveCount(4)
    .And.ContainInOrder(new[] { 2, 3 })
    .And.ContainItemsAssignableTo<int>();
```

2️⃣ **Perform individual assertions on all elements of a collection**

```csharp
var collection = new[]
{
    new { Id = 1, Name = "John", Attributes = new string[] { } },
    new { Id = 2, Name = "Jane", Attributes = new string[] { "attr" } }
};

collection.Should().SatisfyRespectively(
    first =>
    {
        first.Id.Should().Be(1);
        first.Name.Should().StartWith("J");
        first.Attributes.Should().NotBeNull();
    },
    second =>
    {
        second.Id.Should().Be(2);
        second.Name.Should().EndWith("e");
        second.Attributes.Should().NotBeEmpty();
    });
```

If you need to perform the same assertion on all elements of a collection:

```csharp
var collection = new[]
{
    new { Id = 1, Name = "John", Attributes = new string[] { } },
    new { Id = 2, Name = "Jane", Attributes = new string[] { "attr" } }
};

collection.Should().AllSatisfy(x =>
{
    x.Id.Should().BePositive();
    x.Name.Should().StartWith("J");
    x.Attributes.Should().NotBeNull();
});
```

If you need to perform individual assertions on all elements of a collection without setting expectation about the order of elements:

```csharp
var collection = new[]
{
    new { Id = 1, Name = "John", Attributes = new string[] { } },
    new { Id = 2, Name = "Jane", Attributes = new string[] { "attr" } }
};

collection.Should().Satisfy(
    e => e.Id == 2 && e.Name == "Jane" && e.Attributes == null,
    e => e.Id == 1 && e.Name == "John" && e.Attributes != null && e.Attributes.Length > 0);
```

## Exceptions

Imagine you have the `SampleClass` class:

```csharp
public class SampleClass
{
    public void SampleMethod(string? value)
    {
        ArgumentNullException.ThrowIfNull(value);
    }
}
```

With Fluent Assertions, you can check whether a given method throws a specific exception. In this case, using xUnit, we check that `SampleMethod` throws the `ArgumentNullException` exception when the argument is `null`:

```csharp
public class SampleClassTests
{
    [Fact]
    public void SampleMethodTest_WithoutValue_ThrowsException()
    {
        // Arrange
        var subject = new SampleClass();

        // Act
        Action act = () => subject.SampleMethod(null);

        // Assert
        act.Should().Throw<ArgumentNullException>()
            .WithParameterName("value");
    }
}
```

You can also test the case when `SampleMethod` throws no exception, that is, when the argument is other than `null`:

```csharp
public class SampleClassTests
{
    [Fact]
    public void SampleMethodTest_WithValue_NoThrows()
    {
        // Arrange
        var subject = new SampleClass();

        // Act
        Action act = () => subject.SampleMethod("test");

        // Assert
        act.Should().NotThrow();
    }
}
```

Finally, you can also check whether or not a method executed asynchronously throws an exception:

```csharp
public class SampleClass
{
    public Task<string> SampleMethodAsync(string? value)
    {
        return string.IsNullOrEmpty(value)
            ? Task.FromException<string>(new ArgumentException())
            : Task.FromResult(value);
    }
}
```

```csharp
public class SampleClassTests
{
    [Fact]
    public async Task SampleMethodAsync_WithoutValue_ThrowsException()
    {
        // Arrange
        var sampleClass = new SampleClass();
        
        // Act
        Func<Task<string>> act = async () => await sampleClass.SampleMethodAsync(null);
        
        // Assert
        await act.Should().ThrowAsync<ArgumentException>();
    }

    [Fact]
    public async Task SampleMethodAsync_WithValue_NoThrows()
    {
        // Arrange
        var sampleClass = new SampleClass();
        
        // Act
        Func<Task<string>> act = async () => await sampleClass.SampleMethodAsync("test");
        
        // Assert
        await act.Should().NotThrowAsync();
    }
}
```

## Object graph comparison

Consider a `Person` record with an associated `Address` and one or more associated `EmailAddress`:

```csharp
public record Person(string FirstName, string LastName, Address Address, List<EmailAddress> EmailAddresses);

public record Address(string Street, string State, string City, string ZipCode);

public record EmailAddress(string Email, EmailType Type);

public enum EmailType
{
    Private,
    Work
}
```

And to have an equivalent `PersonDto` (also called [DTO](https://en.wikipedia.org/wiki/Data_transfer_object)):

```csharp
public record PersonDto(string FirstName, string LastName, Address AddressDto, List<EmailAddressDto> EmailAddressDtos);

public record AddressDto(string Street, string State, string City, string ZipCode);

public record EmailAddressDto(string Email, EmailType Type);
```

You might want to verify that all exposed members of all objects in the `PersonDto` object graph match the equally named members of the `Person` object graph.

You may assert the structural equality of two object graphs with the `BeEquivalentTo()` method:

```csharp
personDto.Should().BeEquivalentTo(person);
```

Also, you can verify the inequality of two objects with `NotBeEquivalentTo()` method:

```csharp
personDto.Should().NotBeEquivalentTo(person);
```

Let us see below the most common options that can be defined for both methods:

- **Recursion**

By default, the comparison is recursive and recurs up to 10 levels deep. Despite this, you can force recursion as deep as possible with the `AllowingInfiniteRecursion` option:

```csharp
personDto.Should().BeEquivalentTo(person, options => options.AllowingInfiniteRecursion());
```

or if you want to disable recursion just use the `ExcludingNestedObjects` option:

```csharp
personDto.Should().BeEquivalentTo(person, options => options.ExcludingNestedObjects());
```

- **Matching members**

All public members of the `Person` object must be available in the `PersonDto` with the same name. If any member is missing, an exception will be thrown. However, you can only include members that both graph objects have:

```csharp
personDto.Should().BeEquivalentTo(person, options => options.ExcludingMissingMembers());
```

- **Selecting members**

If you want, you can exclude some members using the `Excluding` option:

```csharp
personDto.Should().BeEquivalentTo(person, options => options.Excluding(p => p.FirstName));
```

The `Excluding` option also accepts a lambda expression, which provides more flexibility in deciding which member to exclude:

```csharp
personDto.Should().BeEquivalentTo(person, options => options.Excluding(p => p.FirstName == "John"));
```

You can also decide to exclude a member of a particular nested object based on its index:

```csharp
personDto.Should().BeEquivalentTo(person, options => options.Excluding(p => p.EmailAddresses[1]));
```

You can use `For` and `Exclude` if you want to exclude a member on each nested object regardless of its index:

```csharp
personDto.Should().BeEquivalentTo(person, options => options.For(p => p.EmailAddresses).Exclude(ea => ea.Type));
```

> 📝 **Note**
> `Excluding` and `ExcludingMissingMembers` can be combined.

In a mirrored manner, specific properties or lambdas can be included using the `Including` option:

```csharp
personDto.Should().BeEquivalentTo(person, options => options.Including(p => p.FirstName));
personDto.Should().BeEquivalentTo(person, options => options.Including(p => p.FirstName == "John"));
```

- **Including properties and/or fields**

You can also configure whether to include or exclude the inclusion of all public properties and fields. This behavior can be modified:

```csharp
// Include properties (which is the default)
personDto.Should().BeEquivalentTo(person, options => options.IncludingProperties());

// Include fields
personDto.Should().BeEquivalentTo(person, options => options.IncludingFields());

// Include internal properties as well
personDto.Should().BeEquivalentTo(person, options => options.IncludingInternalProperties());

// And the internal fields
personDto.Should().BeEquivalentTo(person, options => options.IncludingInternalFields());

// Exclude Fields
personDto.Should().BeEquivalentTo(person, options => options.ExcludingFields());

// Exclude Properties
personDto.Should().BeEquivalentTo(person, options => options.ExcludingProperties());
```

- **Comparing members with different names**

Imagine you want to compare a `Person` and a `PersonDto` using `BeEquivalentTo`, but the first type has a `FirstName` property and the second has a `PersonFirstName` property. You can map them using the following option:

```csharp
// Using names with the expectation member name first. Then the subject's member name.
personDto.Should().BeEquivalentTo(person, options => options.WithMapping("FirstName", "PersonFirstName"));

// Using expressions, but again, with expectation first, subject last.
personDto.Should().BeEquivalentTo(person, options => options.WithMapping<PersonDto>(p => p.FirstName, s => s.PersonFirstName));
```

- **Enums**

By default, `BeEquivalentTo()` compares `Enum` members by the underlying numeric value of the `Enum`. However, you can compare an `Enum` by name only by using the `ComparingEnumsByName` option:

```csharp
personDto.Should().BeEquivalentTo(person, options => options.ComparingEnumsByName());
```

## Custom assertions

Creating a custom assertion allows you to encapsulate a more complex assertion logic that can be reused across multiple tests. This can be particularly useful when working with complex or domain-specific assertion logic that is not covered by the predefined assertion methods.

To create a custom assertion, you need to create an extension method for the type of object on which you want to assert. This extension method should contain the assertion logic and can use the existing assertion methods provided by Fluent Assertions. Finally, you need to apply the `[CustomAssertion]` attribute to your extension method to signal that it is a custom assertion method.

Here is an example:

```csharp
public static class PersonAssertions
{
    [CustomAssertion]
    public static void NotBeDoe(this Person? person, string because = "", params object[] becauseArgs)
    {
        Execute.Assertion.ForCondition(person is not null)
            .BecauseOf(because, becauseArgs)
            .FailWith("Expected person not to be null.");
        
        Execute.Assertion.ForCondition(!string.IsNullOrEmpty(person!.FirstName))
            .BecauseOf(because, becauseArgs)
            .FailWith("Expected person's first name not to be null or empty.");
        
        Execute.Assertion.ForCondition(string.Equals(person.LastName, "doe", StringComparison.OrdinalIgnoreCase))
            .BecauseOf(because, becauseArgs)
            .FailWith("Expected person must not have last name Doe.");
    }
}
```

This code first verifies that the `Person` object is not `null`. If it is, it will throw an exception with the message `"Expected person not to be null."`.

Next, it checks whether the `FirstName` of the `Person` object is not `null` or empty. If it is, it will trigger an exception with the message `"Expected person's first name not to be null or empty."`.

Lastly, it confirms that the `LastName` of the `Person` object is not equal to `"doe"`, ignoring case differences. If it is, an exception will be thrown with the message `"Expected person must not have last name Doe."`.

The `because` and `becauseArgs` parameters can be used to specify a custom reason for the assertion to hold true.

This is how to use the custom method:

```csharp
public class PersonTests
{
    [Fact]
    public void Person_WithLastNameDoe_Fails()
    {
        var person = new Person("John", "Doe");
        
        person.NotBeDoe(); // Fails
    }
}
```

## Assertion scopes

`AssertionScope` is a powerful function that allows multiple assertions to be grouped into a single scope. When you use it, all assertions within the scope are executed and any failures are reported together at the end. This is in contrast to the default behavior, in which test execution stops at the first failed assertion.

The advantage of using `AssertionScope` is that you can see all failed assertions in a single test execution, instead of correcting one, rerunning the tests, and then finding the next one. This can be especially useful when writing tests with multiple assertions and you want to get an overview of all the problems at once.

Here is how you can use `AssertionScope`:

```csharp
public record Person(string FirstName, string LastName);
```

```csharp
public class PersonTests
{
    [Fact]
    public void AssertionScopeTest()
    {
        var person = new Person("John", "Doe");

        using (new AssertionScope())
        {
            person.FirstName.Should().Be("Jane");
            person.LastName.Should().Be("Smith");
        }
    }
}
```

In this example, even though the first assertion fails, the second assertion is also executed, and both failures are reported together.

## Execution time

Fluent Assertions also provides a method to assert that the execution time of particular method or action does not exceed a predefined value.

To check the execution time of a method, you can write:

```csharp
public class SampleClass
{
    public string ExpensiveMethod()
    {
        var temp = string.Empty;
        for (var i = 0; i < short.MaxValue; i++)
        {
            temp += i;
        }
        return temp;
    }
}
```

```csharp
public class SampleTests
{
    [Fact]
    public void SampleMethodTests()
    {
        var sampleClass = new SampleClass();
        sampleClass.ExecutionTimeOf(s => s.ExpensiveMethod()).Should().BeLessOrEqualTo(1.Seconds());
    }
}
```

Alternatively, to check the execution time of an arbitrary action, you can write in this other way:

```csharp
Action someAction = () => Thread.Sleep(100);
someAction.ExecutionTime().Should().BeLessThanOrEqualTo(200.Milliseconds());
```

`ExecutionTime` supports the same assertions as `TimeSpan`, namely:

```csharp
someAction.ExecutionTime().Should().BeLessThan(200.Milliseconds());
someAction.ExecutionTime().Should().BeGreaterThan(100.Milliseconds());
someAction.ExecutionTime().Should().BeGreaterThanOrEqualTo(100.Milliseconds());
someAction.ExecutionTime().Should().BeCloseTo(150.Milliseconds(), 50.Milliseconds());
```

If you are dealing with a `Task`, you can also assert that it completed within a specified period of time or not completed:

```csharp
Func<Task> someAsyncWork = () => SomethingReturningATask();
await someAsyncWork.Should().CompleteWithinAsync(100.Milliseconds());
await someAsyncWork.Should().NotCompleteWithinAsync(100.Milliseconds());
await someAsyncWork.Should().ThrowWithinAsync<InvalidOperationException>(100.Milliseconds());
```

## Conclusion

In this article, we have explored just some assertion methods of primitive types, collections, and exceptions, and we have also seen some advanced features such as creating custom assertions or checking the execution time of a single method.

All this leads us to the conclusion that Fluent Assertions is a valuable tool for any developer writing tests in .NET. Because of its emphasis on natural language syntax, it promotes greater readability, which is critical for understanding and maintaining efficient code bases. It also simplifies communication within teams, making it easier to convey the intention behind the tests.

## References

- [Fluent Assertions documentation](https://fluentassertions.com/introduction)
- [Boolean implication](https://mathworld.wolfram.com/Implies.html)