Fluent Assertions first look


I always loved Fluent Interfaces – when done properly they can make an API or library easier to use and understand. I’ve heard of Fluent Assertions before, but I confess I never gave it much attention. I usually use NUnit as my unit testing framework and I just though their API was good enough, and to be honest I didn’t want to waste much time learning yet another library. Just out of curiosity, I decided to take a look into their website today to take a look and try to understand what was the motivation behind Fluent Assertions (emphasis is mine):

Nothing is more annoying than a unit test that fails without clearly explaining why. More than often, you need to set a breakpoint and start up the debugger to be able to figure out what went wrong. (…) That’s why we designed Fluent Assertions to help you in this area. Not only by using clearly named assertion methods, but also by making sure the failure message provides as much information as possible.

I completely agree, sometimes you’re just wasting too much time trying to figure out what went wrong. I have to admit that some of the assertion messages provided by NUnit are not great, so I decided to run some tests and compare the messages between these two libraries.

The scenario – a simple Cache Manager

So consider the following interface for a very simple Cache Manager that I implemented recently:

    public interface ICacheManager : IDisposable
    {
        bool AddOrReplace<T>(string key, T item);

        bool AddOrReplace<T>(CacheObject<T> cacheObject);

        bool TryGetItem<T>(string key, out T item);

        bool TryGetItem<T>(string key, out T item, Func<T> factory);
    }

I had already created an implementation of ICacheManager named MemoryCacheManager and the correspondent unit tests for each of its methods, using NUnit. I have changed some of the unit tests to use Fluent Assertions, in order to compare the assertion messages. These are my results:

Testing Exceptions

The following test checks if a cache key is valid when adding a new cache item. An exception is thrown if the key is null or whitespace. In this case the test should fail, because the cache key is valid.

Running the test using NUnit:

    [Test]
    public void WithInvalidCacheKey_ShouldThrowArgumentException()
    {
        // arrange
        string key = "foo";
        var product = new Product();

        // act
        Action action = () => CacheManager.AddOrReplace(key, product);

        // assert
        Assert.Throws(() => action(), "cache key cannot be null or empty");
    }

Result is the following:
Testing exceptions using NUnit

Using Fluent Assertions:

    [Test]
    public void WithInvalidCacheKey_ShouldThrowArgumentException()
    {
        // arrange
        string key = "foo";
        var product = new Product();

        // act
        Action action = () => CacheManager.AddOrReplace(key, product);

        // assert
        action.Should().Throw("cache key cannot be null or empty");
    }

Result is the following:
Testing exceptions using Fluent Assertions

Testing Booleans

The following test checks if the cache manager returns true when adding a valid CacheObject. I’ve changed the success variable on purpose to force the test to fail, in order to see the assertion messages.

Running the test using NUnit:

    [Test]
    public void WithValidCacheObject_ShouldReturnTrue()
    {
        // arrange
        CacheObject<Product> cacheObject = CacheManagerHelper.CreateCacheObject(new Product());

        // act
        bool success = !CacheManager.AddOrReplace(cacheObject);

        // assert
        Assert.That(success, Is.True, "Adding a valid cache object should return true");
    }

Result is the following:
Testing booleans using NUnit

Using Fluent Assertions:

    [Test]
    public void WithValidCacheObject_ShouldReturnTrue()
    {
        // arrange
        CacheObject<Product> cacheObject = CacheManagerHelper.CreateCacheObject(new Product());

        // act
        bool success = !CacheManager.AddOrReplace(cacheObject);

        // assert
        success.Should().BeTrue("adding a valid cache object should return true");
    }

Result is the following:
Testing booleans using Fluent Assertions

Testing Integers

The following test checks if the cache manager returns the expected value using a factory (delegate). If the item is not in the cache it uses the factory to retrieve it, and then the item is added to the cache. I’ve changed the productId variable on purpose to force the test to fail, in order to see the assertion messages.

Running the test using NUnit:

    [Test]
    public void WithNonExistingItem_ShouldRetrieveNewItem()
    {
        // arrange
        string key = CacheManagerHelper.CreateCacheKey();
        const int newProductId = 2018;
        int Factory() => newProductId;

        // act
        bool success = CacheManager.TryGetItem(key, out int productId, Factory);
        productId = 123; // to force the test to fail

        // assert
        Assert.That(success, Is.True, "trying to get a new item with a factory should return true");
        Assert.That(productId, Is.EqualTo(newProductId), "trying to get a new product Id should return same product Id generated by factory");
    }

Result is the following:
Testing integers using NUnit

Using Fluent Assertions:

    [Test]
    public void WithNonExistingItem_ShouldRetrieveNewItem()
    {
        // arrange
        string key = CacheManagerHelper.CreateCacheKey();
        const int newProductId = 2018;
        int Factory() => newProductId;

        // act
        bool success = CacheManager.TryGetItem(key, out int productId, Factory);
        productId = 123; // to force the test to fail

        // assert
        success.Should().BeTrue("trying to get a new item with a factory should return true");
        newProductId.Should().Be(productId, "trying to get a new product Id should return same product Id generated by factory");
    }

Result is the following:
Test03_FluentAssertions

Testing equality

The following test checks if the cache manager returns an object that is equivalent to the one added to the cache, using the same cache key. I’ve changed the cached object on purpose to force the test to fail, in order to see the assertion messages.

Running the test using NUnit:

    [Test]
    public void WithExistingItem_ShouldRetrieveExpectedProduct()
    {
        // arrange
        string key = CacheManagerHelper.CreateCacheKey();
        var product = new Product("Microsoft surface");
        CacheManager.AddOrReplace(key, product);

        // act
        bool success = CacheManager.TryGetItem(key, out Product cachedProduct);
        cachedProduct = new Product("Amazon Kindle"); // force the test to fail

        // assert
        Assert.That(success, Is.True, "trying to get an item that was added previously to the cache should return true");
        Assert.That(cachedProduct, Is.EqualTo(product), "product from the cache should be equivalent to the original");
    }

Result is the following:
Testing equality using NUnit

Using Fluent Assertions:

    [Test]
    public void WithExistingItem_ShouldRetrieveExpectedProduct()
    {
        // arrange
        string key = CacheManagerHelper.CreateCacheKey();
        var product = new Product("Microsoft surface");
        CacheManager.AddOrReplace(key, product);

        // act
        bool success = CacheManager.TryGetItem(key, out Product cachedProduct);
        cachedProduct = new Product("Amazon Kindle"); // force the test to fail

        // assert
        success.Should().BeTrue("trying to get an item that was added previously to the cache should return true");
        cachedProduct.Should().BeEquivalentTo(product, "product from the cache should be equivalent to the original");
    }

Result is the following:
Testing equality using Fluent Assertions

Conclusion

Fluent Assertions library is very easy to use and the assertion messages are much better compared to NUnit, as you can see above.

The list of assertions is quite extensive and contains not only the typical assertions for strings, numeric types, exceptions, collections etc but also some interesting ones such as Assembly References, which contains methods to assert an assembly does or does not reference another assembly (e.g. to enforce layers within an application) or Execution Time, to assert that the execution time of particular method or action does not exceed a predefined value (e.g. can be used in performance tests).

Even though I barely scratched the surface, I am quite happy with Fluent Assertions and I intend to use it in my next projects.

2 thoughts on “Fluent Assertions first look

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.