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:
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 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:
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 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:
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:
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:
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:
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.
You might get some ideas from https://github.com/gregoryyoung/Simple.Testing which did some other interesting things like actually using expressions and then formatting them with values etc (that code can likely be lifted)
Thanks Greg