ASP.NET Core Unit Testing with FluentAssertions
EzzyLearning.net
Posted on August 21, 2021
Writing and releasing a bug-free software product is a goal of every software developer and one of the tools to achieve this goal is unit testing. Unfortunately, the unit test coverage is not very high in many software projects and the main reason for this low coverage is that many developers think that they are either too busy in writing business logics and can’t give enough time to unit tests or they complain that most of the time unit tests written by other developers are not easy to read. Luckily, we have a library called FluentAssertions that makes our unit test look like the sentences written in plain English which are very easy to write and read by developers. In this tutorial, I will cover different features available in the FluentAssertions library so that you can also develop robust and bug-free software.
What is Unit Testing?
A unit test is a way to test the smallest unit of the software. In almost all programming languages, this smallest unit is normally a function (method), a subroutine, or a property. When we do unit testing, we check whether a particular property has the expected value or a particular method is returning a value or result we are expecting it to return. There are many popular unit test frameworks available for almost all programming languages and all types of applications. The three most popular unit tests frameworks used by many .NET developers around the world are NUnit, xUnit, and MSTest. All of these frameworks have their pros and cons but they all have an extensive set of features for writing unit tests. These three unit test frameworks are so popular that Microsoft Visual Studio has built-in project templates for these frameworks.
FluentAssertions library supports many unit test frameworks including NUnit, xUnit, and MSTest. It has a feature to automatically detect the test framework you are using in your project and it will immediately start using the test framework for throwing framework specific exceptions.
For this tutorial, I will write all unit tests using the NUnit framework which I have been using recently in some of my projects. Here is an example of a simple unit test written using the NUnit framework. Let’s say you have a simple Add method that simply returns the sum of two numbers passed as parameters.
public int Add(int a, int b)
{
return a + b;
}
The following unit test shows how to test the above method using NUnit by adding a [Test] attribute and using the AreEqual method of Assert class.
[Test]
public void TestAddMethod()
{
var result = Add(2, 3);
var expected = 5;
Assert.AreEqual(expected, result);
}
If the Add method is working as expected, our above test should pass and you will see the Success message in the Unit Test session running inside Visual Studio.
Now let’s assume that a developer made some changes in the Add method and by mistake changed the + operator with the – operator in the method.
public int Add(int a, int b)
{
return a - b;
}
If you will run your Unit Test again, your test will fail because you are expecting the value 5 but due to a bug in the Add method, it is not returning what you are expecting. This is the simplest example to demonstrate to you how Unit Tests can help you in identifying the bugs in your method quickly so that you can fix those bugs.
In real-world applications, your methods will not be as simple as our Add method shown in the above example. Maybe you have a method similar to below that returns the array of integers and you want to test it in multiple ways.
public int[] GetNumbers()
{
return new int[] { 1, 2, 3, 4, 5};
}
You want to write unit tests to test all of the following conditions:
- The returned array is not empty
- Array length is 5
- The first value in the array is 1
- The last value in the array is 5
Your unit test will look something like this:
[Test]
public void TestAddMethod()
{
int[] data = GetNumbers();
var expectedLength = 5;
var expectedFirstNumber = 1;
var expectedLastNumber = 5;
Assert.IsNotEmpty(data);
Assert.AreEqual(expectedLength, data.Length);
Assert.AreEqual(expectedFirstNumber, data\[0\]);
Assert.AreEqual(expectedLastNumber, data\[expectedLength - 1\]);
}
There is nothing wrong with the above Unit Test but you need to spend a few seconds if not minutes to understand what’s going on. This is the biggest complaint developers normally have that their unit tests are not easy to read and understand. Before I deep dive into FluentAssertions, let me show you how you can write the above Unit Test using FluentAssertions. The following example shows how you can write a human readable, English style statements to test the same conditions mentioned above using the FluentAssertions library.
[Test]
public void TestAddMethod()
{
int[] data = GetNumbers();
var expectedLength = 5;
var expectedFirstNumber = 1;
var expectedLastNumber = 5;
data.Should().NotBeEmpty();
data.Should().HaveCount(expectedLength);
data.Should().StartWith(expectedFirstNumber);
data.Should().EndWith(expectedLastNumber);
}
You can make your code even shorter by combing multiple conditions with And method as shown below. I am sure you will agree with me that reading the following unit tests is much easier than what we have written without FluentAssertions above.
data.Should().NotBeEmpty()
.And.HaveCount(expectedLength)
.And.StartWith(expectedFirstNumber)
.And.EndWith(expectedFirstNumber);
What is FluentAssertions?
FluentAssertions is one of the most popular (over 66 million downloads on Nuget) .NET library that contains a large collection of .NET extension methods that allow .NET developers to write unit tests using a fluent syntax which is very easy to read and write and clearly shows the intent of the unit test. The library has extension methods to test almost everything related to .NET such as Strings, Booleans, Dates, Guids, Collections, Exceptions, and even Nullable Types. You can add this library to your unit test projects via Nuget package manager and start using this library in few minutes.
Once the library is downloaded from Nuget, you just need to add the following using statement in your unit test file and you have a big list of .NET extensions methods at your disposal.
using FluentAssertions;
Let’s write some basic unit tests to become comfortable with FluentAssertions. The following test is using the most common FluentAssertions method called “Should” which can be chained with many other extension methods of the library. In the following example, we have a string and we want to test whether a string starts with the word “US” or not. Of course, the following test will pass as our currency variable hold the string “US Dollars” which starts with the word “US”.
[Test]
public void TestAddMethod()
{
var currency = "US Dollars";
currency.Should().StartWith("US");
}
If you are expecting a currency string to start with the word “GB” then the following test will fail.
[Test]
public void TestAddMethod()
{
var currency = "US Dollars";
currency.Should().StartWith("GB");
}
You will see a clear message “Expected currency to start with “GB” but “US Dollars” differs near “US “ (index 0).” to show you exactly why the test failed.
You also have an option to customize the above message even further by setting the because parameter available in most FluentAssertions extension methods.
var currency = "US Dollars";
currency.Should().StartWith("GB", because: "We only accepts GB Pounds.");
Running the above test with a custom message will display the following message when the test will fail.
Expected currency to start with "GB" because We only accepts GB Pounds., but "US Dollars" differs near "US " (index 0).
You may have already noticed that the FluentAssertions library is automatically inferring the values we are testing into the test failure messages. This feature is called 'Subject Identification'. In the example above, see how the library is extracting the words like “GB” and “US Dollars” automatically and adding them into the failure message. This is a very useful feature for developers who want to see the data and the reason for their test failures in the quickest way possible.
Another cool feature of FluentAssertions library is the ability to combine multiple unit tests with the And method as shown below:
[Test]
public void TestAddMethod()
{
int[] data = GetNumbers();
data.Should().NotBeEmpty()
.And.HaveCount(5)
.And.StartWith(1)
.And.EndWith(5);
}
public int[] GetNumbers()
{
return new int[] { 1, 2, 3, 4, 5 };
}
Assertion Scopes
If you have multiple Assertions in your unit tests and one of the tests failed, the exception will be thrown immediately and the methods below the failed test will never be called. For example, in the following code snipped the unit test method HaveCount(5) will fail as we have 6 elements in the data array, and the methods StartWith and EndWith will never get executed.
[Test]
public void TestAddMethod()
{
int[] data = GetNumbers();
data.Should().NotBeEmpty();
data.Should().HaveCount(5);
data.Should().StartWith(1);
data.Should().EndWith(5);
}
public int[] GetNumbers()
{
return new int[] { 1, 2, 3, 4, 5, 6 };
}
Running the above unit test will throw the exception with the following message.
Expected data to contain 5 item(s), but found 6.
If you want to run multiple assertions in a batch then you can use AssertionScope which combines multiple assertions in a group and an exception is thrown at the end once the entire batch is executed. If multiple tests will fail, then failure messages of all those tests will be displayed. Here is an example of creating a batch of multiple tests using AssertionScope.
[Test]
public void TestAddMethod()
{
int[] data = GetNumbers();
using (new AssertionScope())
{
data.Should().NotBeEmpty();
data.Should().HaveCount(5);
data.Should().StartWith(1);
data.Should().EndWith(5);
}
}
public int[] GetNumbers()
{
return new int[] { 7, 2, 3, 4, 5, 9 };
}
In the above example, the entire batch of tests will execute and you will see all failure messages in the end as follows:
Expected data to contain 5 item(s), but found 6.
Expected data to start with {1}, but {7, 2, 3, 4, 5, 9} differs at index 0.
Expected data to end with {5}, but {7, 2, 3, 4, 5, 9} differs at index 5.
Basic FluentAssertions Examples
In almost all of the above examples, I have either used strings or an array of integers but please keep in mind that FluentAssertions has a huge collection of methods to test almost every data type or object in .NET. Let’s go through some quick examples of the most common data types we use in our projects on daily basis.
Nullable Types
The common methods to test Booleans are BeNull, NotBeNull, HaveValue, NotHaveValue.
short? number = 5;
number.Should().NotBeNull();
number.Should().HaveValue();
Booleans
The common methods to test Booleans are BeTrue, BeFalse, NotBeTrue, NotBeFalse and Be.
[Test]
public void TestAddMethod()
{
var isAvailable = IsAvailableInStock();
isAvailable.Should().BeTrue();
bool expectedResult = true;
isAvailable.Should().Be(expectedResult);
}
public bool IsAvailableInStock()
{
return true;
}
Strings
FluentAssertions library has a huge collection of methods to test Strings. Here are some common examples and a full list can be found here.
string value = "Fluent Assertions";
value.Should().NotBeNullOrWhiteSpace();
value.Should().Contain("Fluent");
value.Should().Contain("s", Exactly.Thrice());
value.Should().Be("Fluent Assertions");
value.Should().NotBe("FluentAssertions");
value.Should().BeOneOf(
"Fluent Assertions",
"FluentAssertions"
);
DateTime
FluentAssertions library has many methods to test Dates and TimeSpans. Some examples are given below:
DateTime? date = new DateTime(2020, 11, 28);
date.Should().Be(28.November(2020));
date.Should().BeBefore(1.December(2020));
date.Should().NotBeAfter(1.December(2020));
date.Should().HaveDay(28);
date.Should().HaveMonth(11);
date.Should().HaveYear(2020);
DateTime deadline = new DateTime(2020, 11, 30);
date.Should().BeMoreThan(1.Days()).Before(deadline);
Collections
FluentAssertions library also has methods to test Collections and IEnumerable. Here are some common examples and a full list of methods is available here.
IEnumerable collection = new[] { 1, 2, 3, 4 };
collection.Should().NotBeEmpty();
collection.Should().NotContainNulls();
collection.Should().HaveCount(4);
collection.Should().OnlyHaveUniqueItems();
collection.Should().Contain(3);
collection.Should().BeSubsetOf(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Unit Testing ASP.NET Core MVC Controllers
So far we wrote unit tests to test basic .NET types and objects. If you are developing an ASP.NET Core MVC web application, you may want to unit test MVC controllers. Let say you have the following HomeController that returns a list of strings as the model.
public class HomeController : Controller
{
public IActionResult Index()
{
var list = new string\[\] { "One", "Two", "Three" };
ViewData\["ListCount"\] = list.Length;
ViewBag.Message = "Success";
return View("Index", list);
}
}
To unit test this controller, first, you need to add a reference of your MVC project in your unit test project and then you can create an object of HomeController just like a normal C# class. Finally, you can invoke the Index action method as shown below.
[Test]
public void HomeControllerTests()
{
HomeController controller = new HomeController();
var result = controller.Index() as ViewResult;
}
Once you have the ViewResult object available, you can use FluentAssertions methods to test whether our ViewResult is null or not.
result.Should().NotBeNull();
You can also test if the Index action is returning a ViewResult type object.
result.Should().BeOfType(typeof(ViewResult));
You can also write a test to check the data stored in the ViewData dictionary
result.ViewData\["ListCount"\].Should().Be(3);
You can also check the ViewName
result.ViewName.Should().Be("Index");
If you want to test whether the ViewBag.Message property contains the “Success” message, you can write the following code.
((string) controller.ViewBag.Message).Should().Be("Success");
Let’s say you also want to test the Model returned from the Index action. You can achieve this by checking the Model property as shown below. The code snippet below not only checking whether the Model is a type of string[] array but also checking that it’s not empty and has three elements in it.
var model = result.Model as string\[\];
model.Should().BeOfType<string\[\]>();
model.Should().NotBeEmpty();
model.Should().HaveCount(3);
Here is the complete code of all the unit tests we wrote above.
[Test]
public void HomeControllerTests()
{
HomeController controller = new HomeController();
var result = controller.Index() as ViewResult;
result.Should().NotBeNull();
result.Should().BeOfType(typeof(ViewResult));
result.ViewName.Should().Be("Index");
result.Model.Should().NotBeNull();
var model = result.Model as string\[\];
model.Should().BeOfType<string\[\]>();
model.Should().NotBeEmpty();
model.Should().HaveCount(3);
((string) controller.ViewBag.Message).Should().Be("Success");
}
Final Thoughts
In this tutorial, I gave you an overview of one of my favorite Nuget library FluentAssertions. Of course, it’s a huge library and I can’t cover all of its methods in this tutorial. If you want to know more about FluentAssertions then you can read the official documentation here. I hope you will like this library and will start using it in your projects to make your unit tests more human friendly.
Posted on August 21, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.