FluentAssertions: Make Your Unit Tests Easier to Write and Understand
How often it happens to you, that you need to write "too much" supporting code for you unit tests? Let's take a look at the following example:
[Test]
public void CompareDtoAndBusinessClassesBefore()
{
// arrange
UserEntity[] expectedUsers = new[]
{
new UserEntity(1, "Dennis", "Doomen", "ddoomen"),
new UserEntity(2, "Charlie", "Poole", "cpoole")
};
var repository = new Repository();
repository.Users.AddRange(expectedUsers);
// act
var adminService = new AdminService(repository);
IEnumerable<UserDto> actualUsers = adminService.GetAllUsers();
// assert
var sortedExpectedUsers = expectedUsers.OrderBy(user => user.Username);
var sortedActualUsers = actualUsers.OrderBy(user => user.Username);
CollectionAssert.AreEqual(sortedExpectedUsers, sortedActualUsers,
new UserComparer());
}
It represents a typical scenario when testing service oriented applications. You can notice two different classes for describing users:
UserDto
is exposed to the consumers of the serviceUserEntity
is being used inside the application and will usually have more properties than the former one.
Both of them implement a common interface IUser
to make testing easier and maybe even enable some code sharing
elsewhere in the project.
While the arrange and act part could hardly be more concise, there is a lot of plumbing code in the assert part of the
test. UserComparer
probably won't be used outside the tests, but is required for comparing instance of IUser
whether they are of the same type or not. On top of that; we are sorting both collections because results are not
ordered. We should be using CollectionAssert.AreEquivalent
instead of CollectionAssert.AreEqual
here, but there's
no overload accepting an IComparer
available, so we had to find a different solution.
The above sample is using NUnit test framework, but the test wouldn't turn out much differently, if we were using any other test framework. The additional code we need to write has many disadvantages: it means extra work, there can be bugs in the plumbing code, and the tests are more difficult to understand.
While trying to simplify a particularly nasty case of the above described scenario, I stumbled across FluentAssertions NuGet package and I've quickly grown to like it. This is how the above test could look like, when using its assertions:
[Test]
public void CompareDtoAndBusinessClassesAfter()
{
// arrange
UserEntity[] expectedUsers = new[]
{
new UserEntity(1, "Dennis", "Doomen", "ddoomen"),
new UserEntity(2, "Charlie", "Poole", "cpoole")
};
var repository = new Repository();
repository.Users.AddRange(expectedUsers);
// act
var adminService = new AdminService(repository);
IEnumerable<UserDto> actualUsers = adminService.GetAllUsers();
// assert
actualUsers.ShouldAllBeEquivalentTo(expectedUsers);
}
Of course, the arrange and act parts haven't changed. The assert part shrunk down to a single line with no supporting code whatsoever. Although the assertion is named clearly enough, it's you need to know the following to understand the test fully:
- The equivalency of both collections is asserted, i.e. the order is not important, hence there's no sorting required.
- The items in both collections are compared property by property, based on their names, therefore the
IUser
interface is not required any more, as long as properties have the same name.
There's another advantage in using FluentAssertions that can't be noticed by just looking at the test code. If we introduce a bug in the service method, this is the output of the first test:
Expected is <System.Linq.OrderedEnumerable`2
[FluentAssertionsForCollections.UserEntity,System.String]>,
actual is <System.Collections.Generic.List`1
[FluentAssertionsForCollections.UserDto]> with 2 elements
Values differ at index [0]
Expected: <FluentAssertionsForCollections.UserEntity>
But was: <FluentAssertionsForCollections.UserDto>
Yes, we could improve it by overriding ToString()
in both classes, but that would mean even more supporting code for
the sake of test, which we want to avoid.
Here's the output of the second test:
Expected item[0].Id to be 1, but found 2.
Expected item[0].Id to be 1, but found 2.
Expected item[1].Id to be 2, but found 3.
With configuration:
- Include only the declared properties
- Match property by name (or throw)
There is still room for improvement, but unlike the first case; it's quite obvious how both collections differ. This is just another argument in favor of FluentAssertions.
Still, I've just scratched the surface. The library includes a huge collection of assertions which should have you covered, once you get to know them all. If not, it's really easy customize the behavior of many of them with no or next to none supporting code.
There's really no reason not to give it a try. It seems to work with all unit test frameworks currently available. You never know, it might have a big impact on the way you're writing unit tests.