DEV Community

Fabrizio Bagalà
Fabrizio Bagalà

Posted on • Edited on

Fluent Assertions: Fluently Assert the Result of .NET Tests

ℹ️ 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, NUnit, or XUnit 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
Enter fullscreen mode Exit fullscreen mode

Or using the .NET CLI from a terminal window:

dotnet add package FluentAssertions
Enter fullscreen mode Exit fullscreen mode

First assertion

Suppose you have an integer variable result:

var result = 10;
Enter fullscreen mode Exit fullscreen mode

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

result.Should().Be(expectedValue)
Enter fullscreen mode Exit fullscreen mode

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:

int? result = null;

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

result.Should().NotBeNull();
result.Should().NotHaveValue();
Enter fullscreen mode Exit fullscreen mode

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

int? result = 10;

result.Should().Match(x => !x.HasValue);
Enter fullscreen mode Exit fullscreen mode

Booleans

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

var result = true;
var otherBoolean = true;

result.Should().BeTrue();
result.Should().BeFalse();
result.Should().Be(otherBoolean);
result.Should().NotBe(otherBoolean);
Enter fullscreen mode Exit fullscreen mode

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

var result = false;
var otherBoolean = true;

result.Should().Imply(otherBoolean);
Enter fullscreen mode Exit fullscreen mode

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:

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();
Enter fullscreen mode Exit fullscreen mode

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

var result = "This is an example";

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

result.Should().NotBeUpperCased();
result.Should().NotBeLowerCased();
Enter fullscreen mode Exit fullscreen mode

⚠️ 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:

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");
Enter fullscreen mode Exit fullscreen mode

📝 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:

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)
Enter fullscreen mode Exit fullscreen mode

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:

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

result.Should().Match("*@*.com");
Enter fullscreen mode Exit fullscreen mode

Numerics

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

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);
Enter fullscreen mode Exit fullscreen mode

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:

var result = 3.1415927F;

result.Should().BeApproximately(3.14F, 0.01F);
Enter fullscreen mode Exit fullscreen mode

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:

var result = 3.5F;

result.Should().NotBeApproximately(2.5F, 0.5F);
Enter fullscreen mode Exit fullscreen mode

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:

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)
);
Enter fullscreen mode Exit fullscreen mode

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

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);
Enter fullscreen mode Exit fullscreen mode

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

result.Should().BeCloseTo(7.July(2023).At(12, 15), 2.Seconds());
result.Should().NotBeCloseTo(8.July(2023), 1.Hours());
Enter fullscreen mode Exit fullscreen mode

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

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);
Enter fullscreen mode Exit fullscreen mode

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

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);
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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();
    });
Enter fullscreen mode Exit fullscreen mode

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

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();
});
Enter fullscreen mode Exit fullscreen mode

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

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);
Enter fullscreen mode Exit fullscreen mode

Exceptions

Imagine you have the SampleClass class:

public class SampleClass
{
    public void SampleMethod(string? value)
    {
        ArgumentNullException.ThrowIfNull(value);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

        // Assert
        act.Should().NotThrow();
    }
}
Enter fullscreen mode Exit fullscreen mode

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

public class SampleClass
{
    public Task<string> SampleMethodAsync(string? value)
    {
        return string.IsNullOrEmpty(value)
            ? Task.FromException<string>(new ArgumentException())
            : Task.FromResult(value);
    }
}
Enter fullscreen mode Exit fullscreen mode
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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Object graph comparison

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

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
}
Enter fullscreen mode Exit fullscreen mode

And to have an equivalent PersonDto (also called DTO):

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);
Enter fullscreen mode Exit fullscreen mode

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:

personDto.Should().BeEquivalentTo(person);
Enter fullscreen mode Exit fullscreen mode

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

personDto.Should().NotBeEquivalentTo(person);
Enter fullscreen mode Exit fullscreen mode

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:

personDto.Should().BeEquivalentTo(person, options => options.AllowingInfiniteRecursion());
Enter fullscreen mode Exit fullscreen mode

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

personDto.Should().BeEquivalentTo(person, options => options.ExcludingNestedObjects());
Enter fullscreen mode Exit fullscreen mode
  • 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:

personDto.Should().BeEquivalentTo(person, options => options.ExcludingMissingMembers());
Enter fullscreen mode Exit fullscreen mode
  • Selecting members

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

personDto.Should().BeEquivalentTo(person, options => options.Excluding(p => p.FirstName));
Enter fullscreen mode Exit fullscreen mode

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

personDto.Should().BeEquivalentTo(person, options => options.Excluding(p => p.FirstName == "John"));
Enter fullscreen mode Exit fullscreen mode

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

personDto.Should().BeEquivalentTo(person, options => options.Excluding(p => p.EmailAddresses[1]));
Enter fullscreen mode Exit fullscreen mode

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

personDto.Should().BeEquivalentTo(person, options => options.For(p => p.EmailAddresses).Exclude(ea => ea.Type));
Enter fullscreen mode Exit fullscreen mode

📝 Note
Excluding and ExcludingMissingMembers can be combined.

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

personDto.Should().BeEquivalentTo(person, options => options.Including(p => p.FirstName));
personDto.Should().BeEquivalentTo(person, options => options.Including(p => p.FirstName == "John"));
Enter fullscreen mode Exit fullscreen mode
  • 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:

// 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());
Enter fullscreen mode Exit fullscreen mode
  • 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:

// 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));
Enter fullscreen mode Exit fullscreen mode
  • 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:

personDto.Should().BeEquivalentTo(person, options => options.ComparingEnumsByName());
Enter fullscreen mode Exit fullscreen mode

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:

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.");
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

        person.NotBeDoe(); // Fails
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

public record Person(string FirstName, string LastName);
Enter fullscreen mode Exit fullscreen mode
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");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

public class SampleClass
{
    public string ExpensiveMethod()
    {
        var temp = string.Empty;
        for (var i = 0; i < short.MaxValue; i++)
        {
            temp += i;
        }
        return temp;
    }
}
Enter fullscreen mode Exit fullscreen mode
public class SampleTests
{
    [Fact]
    public void SampleMethodTests()
    {
        var sampleClass = new SampleClass();
        sampleClass.ExecutionTimeOf(s => s.ExpensiveMethod()).Should().BeLessOrEqualTo(1.Seconds());
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Action someAction = () => Thread.Sleep(100);
someAction.ExecutionTime().Should().BeLessThanOrEqualTo(200.Milliseconds());
Enter fullscreen mode Exit fullscreen mode

ExecutionTime supports the same assertions as TimeSpan, namely:

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());
Enter fullscreen mode Exit fullscreen mode

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

Func<Task> someAsyncWork = () => SomethingReturningATask();
await someAsyncWork.Should().CompleteWithinAsync(100.Milliseconds());
await someAsyncWork.Should().NotCompleteWithinAsync(100.Milliseconds());
await someAsyncWork.Should().ThrowWithinAsync<InvalidOperationException>(100.Milliseconds());
Enter fullscreen mode Exit fullscreen mode

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

Top comments (6)

Collapse
 
ant_f_dev profile image
Anthony Fung

Thanks for sharing - there's some great info in this article.

BeApproximately seems like a great shortcut for asserting against floating point values, where it'd otherwise be advisable to check that a value is both above a value and below another one too.

Collapse
 
fabriziobagala profile image
Fabrizio Bagalà

Thanks for reading @ant_f_dev

BeApproximately is definitely an interesting feature, and for floating-point types it is ideal. However, my favorite method is BeEquivalentTo which allows you to see if two objects are equal and the ability to customize it with different options makes it very useful.

Collapse
 
_hm profile image
Hussein Mahdi

Is very good ,Thanks 💡🏆

Collapse
 
fabriziobagala profile image
Fabrizio Bagalà

Thank you 😊

Collapse
 
rahul1994jh profile image
Rahul Kumar Jha

great article - it took good amount of time finishing up the read (I guess breaking it up in multi part would be more better for readers)

Collapse
 
fabriziobagala profile image
Fabrizio Bagalà

Thank you for your support 🙏

Initially I had thought of splitting it up, given the length of the article due mainly to the many methods of Fluent Assertions extensions (consider that not all of them are given here otherwise the article would not end 😅).

Subsequently, however, I decided to put it all together so as to have a more "smooth" reading experience.

In any case, I appreciate your suggestion is I will try to adapt for future articles 😉