Enhance your .NET Testing #6: The Art of Assertion with FluentAssertions
FluentAssertions is the Swiss Army knife of assertion libraries for .NET developers. If you want to supercharge your tests to make them more expressive, readable, and robust, this is the tool to have in your arsenal.
In this article, we'll dive deep into the FluentAssertions library, exploring its extensive capabilities and demonstrating how it can simplify your unit testing workflow.
If you didn't read the previous article, I recommend you to do it:
Unpacking FluentAssertions
Developed by Dennis Doomen and first released in 2011, FluentAssertions has since become a beloved tool among .NET developers. Its primary goal is to make your test assertions more expressive, readable, and robust.
It achieves this by providing a fluent and natural syntax that allows you to write human-readable assertions. It's more than just an assertion library; it's a bridge to more intuitive and descriptive testing, finding its application in both simple unit tests and complex integration tests.
To begin using FluentAssertions, you need to install the FluentAssertions
NuGet package. Once installed, you can start writing assertions in a more intuitive and expressive manner. Let's walk through a quick example to get you started.
Consider a scenario where you want to assert that a string variable actualString
is equal to the expected string "Hello, World!"
. In traditional unit testing frameworks, you might write an assertion like this:
Assert.AreEqual("Hello, World!", actualString);
While this assertion gets the job done, it lacks readability and expressiveness. Now, let's see how FluentAssertions can improve this:
actualString.Should().Be("Hello, World!");
With FluentAssertions, the assertion reads like plain English: "actualString should be 'Hello, World!'" – clear, concise, and easy to understand.
As Jonathan Roellinger recommended, FluentAssertions also lets you add error messages related to validations, specifying the reason for the error.
var stringCreator = new StringCreator();
var result = stringCreator.Build("Hello", "World");
// Assert
result.Should().Be("Hello, World!")
.BecauseArgs("Hello", "World") // Using BecauseArgs to specify argument values
.Because("We expect the stringCreator to correctly build the hello world string."); // Providing context
A Deeper Dive into FluentAssertions
Now that we've seen a basic example let's explore some of the most commonly used assertion methods provided by FluentAssertions across various data types:
Generic Assertions
These assertions are versatile and allow you to perform equality checks and null checks with ease. They can be used on all types of objects. Here's how they work:
string? stringValue = "FluentAssertions";
stringValue.Should().Be("FluentAssertions"); // Should be equal to the value
stringValue.Should().NotBe("OtherString"); // Should be different from the value
stringValue.Should().BeNull(); // Should be null
stringValue.Should().NotBeNull(); // Should not be null
String Assertions
Moving on to String Assertions to focus on validating and asserting string-related conditions. These assertions simplify common string checks:
string actualString = "FluentAssertions";
actualString.Should().Contain("Assertions"); // Should contain the specified substring
actualString.Should().StartWith("Fluent"); // Should start with the specified prefix
actualString.Should().EndWith("Fluent"); // Should end with the specified suffix
actualString.Should().MatchRegex("Fluent.*"); // Should match the specified regex pattern
Integer Assertions
Integer Assertions allow to perform checks on integer values, ensuring they meet specific criteria on the value:
int actualValue = 42;
actualValue.Should().BeGreaterThan(30); // Should be greater than the specified value
actualValue.Should().BeInRange(1, 100); // Should be within the specified range
actualValue.Should().BePositive(); // Should be positive
actualValue.Should().BeNegative(); // Should be negative
Boolean Assertions
For Boolean Assertions, it mainly check if the value is true or false:
bool isTrue = true;
isTrue.Should().BeTrue(); // Should be true
isTrue.Should().BeFalse(); // Should be false
DateTime Assertions
Next, DateTime Assertions ensure precise control of date and time conditions for easy comparison :
DateTime date1 = DateTime.Now;
DateTime date2 = date1.AddMinutes(5);
date1.Should().BeBefore(date2); // Should be before the specified date
date2.Should().BeAfter(date1); // Should be after the specified date
date1.Should().BeCloseTo(date2, TimeSpan.FromMinutes(10)); // Should be close to the specified date within the precision
List Assertions
List Assertions provide a powerful means of validating lists and collections. They are undoubtedly the most practical assertions, as they incorporate validations that are more complicated to perform with Assert, notably on the comparison of values:
var numbers = new List<int> { 1, 2, 3, 4 };
numbers.Should().Contain(3); // Should contain the specified item
numbers.Should().NotContain(5); // Should not contain the specified item
numbers.Should().HaveCount(4); // Should have the specified count of elements
numbers.Should().BeEquivalentTo(new List<int> { 4, 3, 2, 1 }); // Should be equivalent to the specified collection
Type Assertions
For Type Assertions, it mainly check if the type or the cast on a type is possible:
object obj = "FluentAssertions";
obj.Should().BeAssignableTo<string>(); // Should be assignable to the specified type
obj.Should().BeOfType<string>(); // Should be of the specified type
Exception Assertions
Exception Assertions make it very easy to catch exceptions to check that a logical part of the application is actually triggered:
Action action = () => { throw new InvalidOperationException(); };
action.Should().Throw<InvalidOperationException>(); // Should throw the specified exception type
action.Should().ThrowExactly<InvalidOperationException>(); // Should throw exactly the specified exception type
action.Should().NotThrow(); // Should not throw any exception
Task Assertions:
Next, Task Assertions guarantee the correct execution and simplify testing asynchronous code:
async Task<int> DelayedTask()
{
await Task.Delay(100);
return 42;
}
await DelayedTask().Should().NotThrowAsync(); // Task should complete successfully without throwing an exception
await DelayedTask().Should().BeCompletedSuccessfully(); // Task should complete successfully without exceptions
HttpResponseMessage Assertions (for testing HTTP responses):
Finally, we look at HttpResponseMessage Assertions for testing HTTP responses when tests are combined with an HTTP call, as with the WebApplicationFactory:
var httpResponse = new HttpResponseMessage(HttpStatusCode.OK);
httpResponse.Headers.Add("Content-Type", "application/json");
httpResponse.Content = new StringContent("{\"message\":\"Hello, World!\"}");
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); // Should have the specified status code
httpResponse.Should().HaveHeader("Content-Type"); // Should have the specified header
httpResponse.Should().HaveContent("{\"message\":\"Hello, World!\"}"); // Should have the specified content
These assertions are adaptable to a wide range of scenarios, allowing you to express your test expectations in a clear and concise manner.
Summary
By using FluentAssertions, you can write unit tests that read like plain English sentences, making them more understandable to both developers and non-developers. The expressive syntax not only enhances the quality of your tests but also simplifies the process of identifying and resolving issues.
Incorporating FluentAssertions into your testing workflow empowers you to write elegant, maintainable, and effective unit tests, ultimately leading to more robust and reliable software.
Next article:
To go further:
Have a goat day 🐐