Unit tests are usually a must-have part of building and maintaining a software application. Fluent Assertions is a wonderful library of extension methods. Due to the nature of the library, the code reads like a written sentence. In this article, we will review many methods Fluent Assertions offers as well as its unique features.
Let’s start.
Installing Fluent Assertions
First, let’s install the Fluent Assertions NuGet package in our project:
Install-Package FluentAssertions
We can begin using Fluent Assertions by adding using FluentAssertions;
to our code.
Installing this package into our solution allows us to use the assertion extension methods and Fluent Assertions’ other features.
How Fluent Assertions Should() Work
The goal of Fluent Assertions is to make unit tests easier to write and read. Fluent Assertions’ unique features and a large set of extension methods achieve these goals. These extension methods read like sentences. This makes it easy to understand what the assertion is testing for. Additionally, test failures by Fluent Assertions are very clear as to why the Assertion has failed.
Let’s consider the assertion example:
var myTestString = "Hello, this is a test string"; myTestString.Should().StartWith("He").And.EndWith("g").And.HaveLength(28);
As we can read this test assertion states that the string myTestString
should start with "He"
and end with "g"
and have a length of 28 characters.
Now let’s look at the same assertion without Fluent Assertions:
Xunit.Assert.StartsWith("He", myTestString); Xunit.Assert.EndsWith("g", myTestString); Assert.AreEqual(28, myTestString.Length);
While we can see this is an equivalent test, it is much longer and a bit harder to read compared to the previous assertion we have.
The Should()
extension method is an interesting method. To understand why this method is useful it’s important to note that all Fluent Assertion extension methods are part of one static class. This results in all extension methods being available to use. The Should()
method will only make available the extension methods that apply to the object type being asserted.
For example, consider we want to make an assertion over an integer variable. Using Should()
will expose only assertion methods used to evaluate numeric value types such as BeGreaterThan()
, BeLessThanOrEqualTo()
, and BeNegative()
.
Assertion Methods
In this section, we will explore commonly used assertion methods for the most common data types in C#.
String and Regex Assertion Methods
The methods we are going to talk about are extremely useful because strings are ubiquitous in any programming.
The Be()
method will compare the subject-under-test (SUT) and the given parameter and will pass the assertion only if it is an exact match. The BeEquivalentTo()
method behaves the same as Be()
but instead will ignore casing differences.
BeEmpty()
will check if the SUT is an empty string. Not to be confused with BeNull()
which will check if the SUT object is null
. If the goal is to assert one or the other we can use BeNullOrEmpty()
.
BeLoweredCased()
and BeUpperCased()
will test if the SUT is comprised of all lower or upper case characters.
Conversely, there are also ‘Not’ variants of these extension methods that test the opposite of the previously mentioned methods. Examples of these are NotBe()
, NotBeEquivalentTo()
, NotBeNull()
, NotBeNullOrEmpty()
, and NotBeUpperCased()
:
var myTestString = "hello, world"; var stringSimilarToMyTestString = "HELLO, WORLD"; var myEmptyString = ""; string? myNullString = null; myTestString.Should().Be("hello, world"); myTestString.Should().BeEquivalentTo(stringSimilarToMyTestString ); myNullString.Should().BeNull(); myEmptyString.Should().BeEmpty(); myEmptyString.Should().BeNullOrEmpty(); myTestString.Should().BeLowerCased(); stringSimilarToMyTestString.Should().BeUpperCased();
Moreover, there are also much more robust extension methods that enable us to match a SUT to a regular expression. We can match a simple pattern with only two characters *
and ?
using Match()
. If letter casing is not important for the assertion we can perform a simple pattern matching with MatchEquivalentOf()
. We can match a true regular expression using MatchRegex()
:
var myTestString = "Hello, this is a test string"; var myDateString = "05/30/2022"; myTestString.Should().MatchEquivalentOf("HElLo, this is a * string"); myDateString.Should().MatchRegex("\\d{1,2}\\/\\d{1,2}\\/\\d{4}");
There are also ‘Not’ variants of these methods such as NotMatch()
, NotMatchEquivalentOf()
, NotMatchRegex()
. In general, there typically exists a ‘Not’ version of most assertion methods.
Collections Assertion Methods
There are many assertion methods for collections that Fluent Assertions offers. While this is not a complete list, let us discuss some important methods that we can use to build robust, layered assertions.
First, let’s examine a few methods that are concerned with the order of our collection.
The HaveElementAt(int index, T element)
method will compare the element parameter to the element in the SUT collection at the index parameter.
ElementPreceding(T successor, T expectation)
and ElementSucceeding(T predecessor, T expectation)
will assert that the expectation parameter is preceding or succeeding respectively.
BeInAscendingOrder()
and BeInDescendingOrder()
can be used to validate a collection that has been ordered. For built-in data types, this happens natively without the need for passing parameters. In general, for more complex, custom data types the class must implement IComparer
. Alternatively, and more conveniently, Fluent Assertions provides overloads that allow us to define a property to use as a comparison point:
customers.Should().HaveElementAt(0, customer); customers.Should().HaveElementPreceding(customer2, customer); customers.Should().HaveElementSucceeding(customer, customer2);
Let’s explore this feature as we will see there are many instances that this library offers capabilities like this one. Consider a Customer
class that has an int Id
property. BeInAscendingOrder()
asserts that the elements of a collection are ordered by the indicated property:
myCustomerList.Should().BeInAscendingOrder(customer => customer.Id); myOtherCustomerList.Should().BeInDescendingOrder(customer => customer.Id);
Next, let’s examine Contain()
. This method will search the SUT to see if it contains a provided expected element parameter:
var customers = new List<Customer>(); var customer = new Customer(1); customers.Add(customer); customers.Should().Contain(customer);
Let’s just mention a few more useful methods.
IntersectWith()
asserts that a provided collection parameter has at least one element in common with the SUT collection.
A useful method is OnlyHaveUniqueItems()
. This method asserts that the SUT collection only contains unique elements without repeated elements.
Equal()
will examine if the SUT collection and the expected collection parameter are equal in elements and order of elements.
Lastly, let’s explore AllSatisfy()
. This is a robust method that asserts that all elements of the SUT collection satisfy a predicate:
myCustomerList.Should().AllSatisfy(customer => customer.Id.Should().BeGreaterThan(0));
Here, we assert that all elements of myCustomerList
satisfy the predicate that states customer.Id
is greater than zero.
Meta-Object Assertion Methods
This section will discuss Fluent Assertions that analyze the SUT as an object.
First, we will examine OfType<T>()
. This method asserts that the SUT object is of type T
.
BeSameAs(T expected)
will test if the SUT is the same object as the expected parameter. For this assertion to succeed it is not enough that the SUT is equal to the expected object. The SUT must be references to the same object in memory.
Lastly, BeAssignableTo<T>()
performs a similar assertion as OfType()
but the subtle difference is that with BeAssignableTo<T>()
can test for inheritance:
// Special Customer inherits from Customer var specialCustomer = new SpecialCustomer(); var customer = new Customer(1); customer.Should().BeOfType<Customer>(); customer.Should().BeSameAs(customer); specialCustomer.Should().BeAssignableTo<Customer>();
There are a lot of assertions that test the implementation of a class itself. These assertions must be made on a Type
object.
We can also make assertions about the methods, properties, and attributes of a class.
BeStatic()
BeSealed()
and BeAbstract()
can test for all of these class modifiers.
BeDecoratedWith<T>()
tests if the SUT’s class has been decorated with attribute T
. For example, this could be useful in testing if all controller classes in a project have been decorated with [
. ]
We can build assertions about methods by first calling GetMethods()
, filtering down what methods we are testing for, and lastly building our assertion:
typeof(myApiController).Methods() .ThatReturn<ActionResult>() .ThatAreDecoratedWith<HttpPostAttribute>() .Should() .BeAsync() .And.Return<ActionResult>();
In this assertion after we obtain all methods of our myApiController
class, we filter this collection by filtering for methods that only return ActionResult
and are decorated with the HttpPost
attribute. We then begin the assertion by calling Should()
and asserting that all methods in our SUT collection of methods are async
and return ActionResult
.
Fluent Assertions offer many methods like these to analyze the implementation of a class. For more information on methods like these, we can visit the Fluent Assertions documentation.
Exception Related Assertion Methods
In this section, we will discuss how to handle exceptions in our unit tests. It is a good practice to test code for expected exception behavior. Fluent Assertions offers extension methods that allow us to easily and intuitively write exception-related assertions.
Let’s discuss three ways to write the same assertion. We will assert that the Customer
class’ GetId()
method will throw a NullReferenceException
if the Id
property is null
:
// Customer Object with null Id property var customer = new Customer(); // Asserts that calling GetId() will throw customer.Invoking(c => c.GetId()).Should().Throw<NullReferenceException>(); // Asserts that calling GetId() will throw Action action = () => customer.GetId(); action.Should().Throw<NullReferenceException>(); // Asserts that calling GetId() will throw FluentActions.Invoking(() => customer.GetId()).Should().Throw<ArgumentNullException>();
We create three assertions that all test the same thing: a Customer
object with a null Id
property will throw an exception when GetId()
is called.
In the first assertion, we use the Invoking()
extension method on the customer object. Next, we define an Action
that calls the GetId()
method. We then assert that the action will throw an exception. Lastly, we use the Invoking()
static method in the FluentActions
class and provide it with a lambda expression that calls GetId()
.
Finally, let’s examine how to handle asynchronous functions that may throw an exception:
Func<Task> act = () => customer.GetIdAsync(); await act.Should().ThrowAsync<NullReferenceException>();
ThrowAsync()
tests if calling GetIdAsync()
will throw a NullReferenceException
.
Assertion Scope
Fluent Assertions allows us to define an AssertionScope
in a using statement. To be able to use AssertionScope
we must include using FluentAssertions.Execution;
in our code file.
In general, the purpose of this feature is to allow us to evaluate all assertions defined within the scope even if a previous assertion has failed. Let’s write a few assertions within an AssertionScope
:
using (new AssertionScope()) { myCustomerList.Should().NotBeNull(); myCustomerList.Should().HaveCountGreaterThan(0); myCustomerList.Should().OnlyHaveUniqueItems(); myCustomerList.Should().AllBeOfType<Customer>(); }
In this example, previous failures do not affect other assertions in the assertion scope. Consequently, this can be useful as it would allow us to write tests that may make it easier to find the cause of a failure.
Assertion Chaining
Fluent Assertions offers properties on assertion methods that allow us to chain together multiple methods that allow us to write more complex, robust assertions.
First, let’s begin by discussing And
and Then
. These properties allow us to add assertions after an assertion:
myCustomerList.Should().NotBeNull().And.HaveCountGreaterThan(0); Execute.Assertion.FailWith("myCustomerList is empty").Then.ForCondition(myCustomerList.Count > 0);
Now let’s examine Which
.
This allows us to chain assertions by returning the single result of the previous assertion. Let’s look at an example where we make an assertion over a collection then we chain it with an assertion about an element in the collection:
myCustomerList.Should().ContainSingle().Which.Should().BeEquivalentTo(new Customer { Id = 99 });
Lastly, Where
can also be used to chain assertions together. For example, let us consider an assertion about an exception thrown from a previous assertion:
Action action = () => customer.GetId(); action.Should().Throw<NullReferenceException>().Where(ex => ex.Message != null).Where(ex => ex.Data.Count > 0);
Assertion Failures are Readable
One common theme to all methods in the Fluent Assertion library is that they all provide readable errors and assertion failures. It can be extremely frustrating dealing with a coding error that produces an unreadable stack trace.
Fluent Assertions assertion failures are very readable by default. Every assertion method has an optional because
parameter. As a result, this parameter justifies the assertion no matter if it passed or failed:
[TestMethod] public void CheckIdGreaterThanReservedValuesTest() { int id = -5; id.Should().BeGreaterThan(5); }
This assertion fails with the message:
Expected id to be greater than 5, but found -5 (difference of -10).
This is easily understandable, but let’s add more context by including a because
parameter string:
[TestMethod] public void CheckIdGreaterThanReservedValuesTest() { int id = -5; id.Should().BeGreaterThan(5, "because Id values under five are reserved"); }
This assertion fails with the message:
Expected id to be greater than 5 because Id values under five are reserved, but found -5 (difference of -10).
In essence, the because
parameter enables us to write much more custom, helpful messages.
Conclusion
In conclusion, we have covered many methods and unique features of Fluent Assertions. One of the best features of Fluent Assertions is the ability to make clear, readable tests. This is achieved by providing the because
parameter on all assertion methods and all methods are intuitively named to read like sentences. Fluent Assertions offers many assertions methods out of the box and is easily extensible to write custom assertions.