Why Your Unit Test in C# Needs Fluent Assertions
Why Your Unit Test in C# Needs Fluent Assertions

Why Your Unit Test in C# Needs Fluent Assertions

There’s one big difference between being a good programmer and a great one.

A great one is always thinking about the future of the software. He thinks about how he can write code to be easy to read and understand.

One of the best ways to improve the readability of the unit testing is to use Fluent Assertions. What are Fluent Assertions?

Fluent Assertions are a set of extension methods for assertions in unit testing to make the assertions more readable and easier to understand. In addition, they allow you to chain together multiple assertions into a single statement.

Fluent assertions are a potent tool that can make your code more expressive and easier to maintain. This article will explain why Fluent Assertions is the most powerful and valuable testing framework for .NET developers.

What is Fluent Assertions?

fluent assertions

Fluent Assertions is a library for asserting that a C# object is in a specific state.

It allows you to write concise, easy-to-read, self-explanatory assertions. The main advantage of using Fluent Assertions is that your unit tests will be more readable and less error-prone. This is because Fluent Assertions provides many extension methods that make it easier to write assertions.

What does fluent mean in the name? Well, fluent API means that the library relies on method chaining. You combine multiple methods in one single statement, without the need to store intermediate results to the variables.

Consider the following code:

Assert.Equal("9780321146533", book.ISBN);
Assert.Equal('3', book.ISBNCheckDigit);
Assert.Equal("Kent Beck", book.Author);
Assert.Equal("Test Driven Development: By Example", book.Name);

You can see how this gets tedious pretty quickly. The code between each assertion is nearly identical, except for the expected and actual values. Each assertion also has a similar format, making the unit test harder to read.

This same test with fluent assertions would look like this:

book.ISBN.Should().Be("9780321146533");
book.ISBNCheckDigit.Should().Be('3');
book.Author.Should().Be("Kent Beck");
book.Name.Should().Be("Test Driven Development: By Example");

Much cleaner, right?

The chaining of the Should and Be methods represents a fluent interface.

The code flows out naturally, making the unit test easier to read and edit. The Should extension methods make the magic possible. The above statements almost read like sentences in plain English:

  • The book’s ISBN should be 9780321146533.
  • The book’s ISBN check digit should be 3.
  • The book’s author should be Kent Beck.
  • The book’s name should be “Test Driven Development: By Example”.

In addition, Fluent Assertions provides many other extension methods that make it easy to write different assertions.

Why are Fluent Assertions important in unit testing in C#?

Code needs to be readable in software development because it makes it easier for other developers to understand and contribute to the code base. Additionally, readable code is more maintainable, so you need to spend less time making changes to it.

Fluent Assertions are important in unit testing because they allow the code to be easily read and followed. This makes it easier to determine whether or not an assertion is being met. As a result, everyone can easier read and understand unit tests, making it easier to locate the failing assert.

In addition to more readable code, the failing test messages are more readable. For example, let’s use the following test case:

public void Test_bool_output()
{
     bool saveOperationResult = false;
     Assert.True(saveOperationResult);
}

Imagine that, instead of hardcoding the result variable to false, you call a method that returns a boolean variable. When this test fails, the output is formatted as follows:

Assert.True() Failure
Expected: True
Actual: False

Let’s compare that with the following test:

public void Test_bool_output()
{
      bool saveOperationResult = false;
      saveOperationResult.Should().BeTrue();
}

The output message in this case is:

Expected saveOperationResult to be true, but found False.

Again, much clearer, right? By looking at the error message, you can immediately see what is wrong.

Unit testing is an essential part of any software development process. By writing unit tests, you can verify that individual pieces of code are working as expected. As a result, they increase the quality of your codebase, and they reduce the risk of introducing bugs.

How to add Fluent Assertions to your project

It’s easy to add fluent assertions to your unit tests. Just add the FluentAssertions NuGet package through the CLI:

Install-Package FluentAssertions

Alternatively, you can add it to your project inside Visual Studio by going to Manage Nuget Packages… and selecting the FluentAssertions NuGet package:

Install Fluent Assertions NuGet package
Install NuGet Package

You might notice the package is trendy. It has over 129 million downloads, making it one of the most popular NuGet packages.

Examples of Fluent Assertions

Now that you have Fluent Assertions installed, let’s look at some examples.

Subject identification – Fluent Assertions Be()

The first example is a simple one. We want to check if an integer is equal to 5:

int number = 5;
number.Should().Be(5);

You can also include an additional message to the Be method:

int number = 6;
number.Should().Be(5, "because that is the correct amount");

When the above assert fails, the following error message will be displayed in the Test output window:

Expected number to be 5 because that is the correct amount, but found 6.

A little bit of additional information for the error message parameter:

A formatted phrase as is supported by System.String.Format(System.String,System.Object[]) explaining why the assertion is needed. If the phrase does not start with the word because it is prepended automatically.

Basic assertions

All reference types have the following assertions available to them.

sut.Should().BeNull();
sut.Should().NotBeNull();

sut.Should().BeOfType<Customer>();
sut.Should().Be(otherCustomer);

In the above case, the Be method uses the Equals method on the type to perform the comparison.

Next, you can perform various assertions on the strings:

"string".Should().BeNullOrEmpty();
"string".Should().BeNullOrWhiteSpace();

"string".Should().NotBeNullOrEmpty();
"string".Should().NotBeNullOrWhiteSpace();

Booleans have BeTrue and BeFalse extension methods.

result.Should().BeTrue();
result.Should().BeFalse();

When it comes to performing asserts on numeric types, you can use the following options:

var integer = 1;

integer.Should().Be(1);
integer.Should().NotBe(10);

integer.Should().BePositive();
integer.Should().BeNegative();

integer.Should().BeGreaterThanOrEqualTo(88);
integer.Should().BeGreaterThan(66);
integer.Should().BeLessThanOrEqualTo(56);
integer.Should().BeLessThan(61);
integer.Should().BeInRange(1, 5);
integer.Should().NotBeInRange(6, 9);

BeEquivalentTo – Object graph comparison

BeEquivalentTo extension method is a powerful way to compare that two objects have the same properties with the same values. The two objects don’t have to be of the same type. The method checks that they have equally named properties with the same value. Following is a full remark of that method, taken directly from the code:

Objects are equivalent when both object graphs have equally named properties with the same value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another, and the result is equal. The type of a collection property is ignored as long as the collection implements System.Collections.Generic. IEnumerable’1 and all items in the collection are structurally equal. Notice that actual behavior is determined by the global defaults managed by FluentAssertions.AssertionOptions.

The example:

customer.Should().BeEquivalentTo(customerDto);

It’s quite common to have classes with the same properties. That’s especially true these days, where it’s common for API methods to take a DTO (Data Transfer Object) as a parameter.

Fluent Assertions Be vs BeEquivalentTo

What is the difference between Be and BeEquivalentTo methods? Be extension method compares two objects based on the System.Object.Equals(System.Object) implementation. BeEquivalentTo method compares properties and it requires that properties have the same names, no matter the actual type of the properties.

Fluent Assertions And

To chain multiple assertions, you can use the And constraint. The example:

collectionToTest.Should().Contain("first")
.And.HaveElementAt(2, "third");

Collection extension methods

There are plenty of extension methods for collections. Let’s see the most common assertions:

var collectionToTest = new List<string>
{
  "first",
  "second",
  "third"
};

collectionToTest.Should().NotBeEmpty();
collectionToTest.Should().HaveCount(3);

collectionToTest.Should().Equal(new List<string> { "first", "second", "third" });
collectionToTest.Should().Equal("first", "second", "third");
collectionToTest.Should().BeEquivalentTo(new List<string> { "first", "second", "third" });
collectionToTest.Should().NotBeEquivalentTo(new List<string> { "1", "2", "3" });

collectionToTest.Should().OnlyHaveUniqueItems();
collectionToTest.Should().HaveCountGreaterThan(2);
collectionToTest.Should().HaveCountGreaterThanOrEqualTo(3);
collectionToTest.Should().HaveCountLessThanOrEqualTo(5);
collectionToTest.Should().HaveCountLessThan(5);
collectionToTest.Should().NotHaveCount(1);

collectionToTest.Should().StartWith("first");
collectionToTest.Should().StartWith(new List<string> { "first" });
collectionToTest.Should().EndWith("third");
collectionToTest.Should().EndWith(new List<string> { "second", "third" });
collectionToTest.Should().Contain("first")
  .And.HaveElementAt(2, "third");

collectionToTest.Should().BeEmpty();
collectionToTest.Should().BeNullOrEmpty();
collectionToTest.Should().NotBeNullOrEmpty();
collectionToTest.Should().ContainInOrder(new List<string> { "first", "second", "third" });
collectionToTest.Should().NotContainInOrder(new List<string> { "1", "2", "3" });

It is also possible to check that the collection contains items in a certain order with BeInAscendingOrder and BeInDescendingOrder.

collectionToTest.Should().BeInAscendingOrder();
collectionToTest.Should().BeInDescendingOrder();

collectionToTest.Should().NotBeInAscendingOrder();
collectionToTest.Should().NotBeInDescendingOrder();

Date and time assertions

The extension methods for checking date and time variables is where fluent API really shines. The following examples show how to test DateTime.

DateTime date = new DateTime(2022, 2, 7);

date.Should().Be(7.February(2022).At(0,0));
date.Should().BeAfter(6.February(2022).At(23,59));
date.Should().BeBefore(8.February(2022).At(0,1));
date.Should().BeSameDateAs(7.February(2022).At(0,0));

date.Should().NotBe(6.February(2022));
date.Should().NotBeAfter(8.February(2022).At(10, 28));
date.Should().NotBeBefore(1.January(2022));
date.Should().NotBeSameDateAs(27.February(2022));

AssertionScope

You can use an AssertionScope to combine multiple assertions into one exception. This makes your test code much cleaner and easier to read.

int numberOfDocuments = 5;

using (new AssertionScope())
{
     numberOfDocuments.Should().Be(6);
     "First Name".Should().Be("Last Name");
}

The above will display both failures and throw an exception at the point of disposing the AssertionScope with the following format:

Message: 

Expected numberOfDocuments to be 6, but found 5.
Expected string to be "Last Name" with a length of 9, but "First Name" has a length of 10, differs near "Fir" (index 0).

Check for exceptions with Fluent Assertions

Now let’s try to use Fluent Assertions to check if the exception is thrown:

Action act = () => sut.BadMethod();
act.Should().Throw<ArgumentException>();

On the other hand, if you want to check that the method doesn’t throw, you can use NotThrow method:

Action act = () => sut.GoodMethod();
act.Should().NotThrow<NullReferenceException>();

Fluent Assertions also support asynchronous methods with ThrowAsync:

Func<Task> act = () => sut.ThisMethodThrowsAsync();

await act.Should().ThrowAsync<ArgumentNullException>();

await act.Should().NotThrowAsync();

How to write a custom assertion using Fluent Assertions?

Fluent Assertions is extensible. You can write your custom assertions that validate your custom classes and fail if the condition fails.

The following custom assertion looks for @ character in an email address field. The email variable is a string. That’s why we are creating an extension method that takes StringAssertions as a parameter.

public static class EmailStringExtensions
{
    public static AndConstraint<StringAssertions> ContainAtSign(this StringAssertions emailString,
                                                                string because = "",
                                                                params object[] becauseArgs)
    {
        Execute.Assertion
            .BecauseOf(because, becauseArgs)
            .ForCondition(emailString.Subject.Contains('@'))
            .FailWith("Expected email to contain @{reason}.");

        return new AndConstraint<StringAssertions>(emailString);
    }
}

 And the usage:

string email = "test@methodpoet.com";
email.Should().ContainAtSign("because that is valid email");

You can also write custom assertions for your custom classes by inheriting from ReferenceTypeAssertions.

What are some alternatives to Fluent Assertions?

The most popular alternative to Fluent Assertions is Shouldly. A Shouldly assertion framework is a tool used for verifying the behavior of applications. It allows developers to write assertions about the expected behavior of their code and then verify that those assertions hold true. This can help ensure that code behaves as expected and that errors are caught and reported early.

Fluent Assertions vs Shouldly: which one should you use? They are pretty similar, but I prefer Fluent Assertions since it’s more popular. In addition, there are higher chances that you will stumble upon Fluent Assertions if you join an existing project.

Should you use Fluent Assertions in your project?

Yes, you should. There are many benefits of using Fluent Assertions in your project. Fluent assertions make your tests more readable and easier to maintain. In addition, they improve the overall quality of your tests by providing error messages that have better descriptions. In some cases, the error message might even suggest a solution to your problem!

Conclusion

It’s not enough to know how to write unit tests. You also need to write readable tests. One of the best ways is by using Fluent Assertions. This library allows you to write clearly-defined assertions that make it easy for anyone who reads your tests to understand exactly what they are testing.

This article presented a small subset of functionality. There is a lot more to Fluent Assertions. You can find more information about Fluent Assertions in the official documentation.

In case you want to learn more about unit testing, then look at unit testing in the C# article. It is a one-stop resource for all your questions related to unit testing.