Fluent Assertions Reloaded
If you’re not using fluent assertions and rely on basic JUnit assertions, you might be missing out on a simple but effective tool to write high-quality test code.
Join the DZone community and get the full member experience.
Join For FreeIf you’re not using fluent assertions in your tests yet and just rely on basic JUnit assertions, you might be missing out on a simple but very effective tool to write high-quality test code. The article starts with a short recap about the virtues of fluent assertions and how they address the shortcomings of basic assertions.
But while very much in favor of fluent assertion I think that current implementations don’t realize their full power: Problems are demonstrated in AssertJ, the main fluent assertion library for Java. Finally, I present a new solution to overcome these issues.
Why You Better Not Only Rely On JUnit Assertions
Test frameworks like JUnit or TestNG are used to run tests and can help to implement them. For the latter JUnit offers a basic set of static assertion methods defined in org.junit.Assert
(in JUnit4, or org.junit.jupiter.api.Assertions
in JUnit5, or org.testng.Assert
in TestNG) which allows expressing expectations on values under test.
But when overused these basic assertions become problematic: They are tedious to write, not very readable and their error messages too often are not helpful - you know that you are doing something wrong when you need to debug your test to further diagnose the error. assertTrue
is the main partner in crime:
import static org.junit.Assert.assertTrue;
Set<?> set = ...
assertTrue(set.contains(″A″));
Enter Fluent Assertions
Alex Ruiz to my knowledge pioneered the idea in 2007 with his FEST Assert library for Java. Since then numerous implementations have been created in all kinds of programming languages. In Java, AssertJ can be regarded as the current main fluent assertion library.
These libraries promise to provide a better way to write assertions than static assertion methods: The solution builds on a – in retrospect – simple idea: Replace static assertion methods by assertion objects: A (type-specific) assertion object wraps the actual value under test and provides a fluent API to state expectations on that value:
import static org.assertj.core.api.Assertions.assertThat;
String actual = ...
assertThat(actual).startsWith(″hello″).contains(″world″);
Let’s decode the example:
- A polymorphic static entry point (in AssertJ it is
Assertion.assertThat
creates a type-specific assertion object (e.g. an AssertJStringAssert
) for an actual value. - The assertion object provides a fluent API to express and chain expectations on the actual value (e.g. methods
startsWith
,contains
inStringAssert
, mirroring methods in theString
class). - As usual, an
AssertionError
is thrown when the assertion implemented by one of these methods fails.
The improvements over static assertion methods are obvious:
- The fluent API coupled with IDE code completion makes writing such assertions fast and easy.
- It produces dense, highly readable test code with an excellent assertion to code ratio.
- In case of an assertion failure, the assertion object knows about the actual value and the assertion context (tested property, passed parameters, etc.) to be able to build a truly helpful error message.
AssertJ: A Fluent API On The Edge
Despite the advantages of fluent assertion compared to basic assertions, I think that current fluent assertions libraries show design flaws that decrease their value. That thesis is showcased using AssertJ.
AssertJ is a vibrant open-source project which provides fluent assertions for Java core classes and popular Java libraries. Created in 2013 by Joel Costigliola as a fork of FEST Assert it has a huge number of contributors who constantly increase and refine assertion coverage. But this admirable effort to seek completeness has produced assertion classes with crowded if not bloated APIs. These risks dilute the advantage of a fluent API, namely to use code completion of your IDE to speed type your method calls. We highlight 3 examples:
a) API Duplication Between Assertion Classes
Given an actual String value we might want to state expectations on its length:
import static org.assertj.core.api.Assertions.assertThat;
String s = ...
assertThat(s).hasSize(10);
But equality is just the most basic assertion. What about other expectations on numeric values? AssertJs StringAssert
additionally provides these methods to state assertions on the String length:
hasSizeBetween(int lowerBoundary, int higherBoundary)
hasSizeGreaterThan(int expected)
hasSizeGreaterThanOrEqualTo(int expected)
hasSizeLessThan(int expected)
hasSizeLessThanOrEqualTo(int expected)
Looking at the API of AssertJ's IntegerAssert
which provides assertions on int/Integer
values we see that the String length assertions quasi duplicate part of the IntegerAssert
API:
isBetween(Integer start, Integer end)
isGreaterThan(int other)
isGreaterThanOrEqualTo(int other)
isLessThan(int other)
isLessThanOrEqualTo(int other)
b) API Increase Due to Method Variations to Cover a Complex Aspect
AssertJ’s ListAssert
offers about 19 positive and 8 negative assertions to check if a List contains some value.
contains(T...)
containsAnyElementsOf(Iterable<T>)
containsAnyOf(T...)
containsExactly(T...)
containsExactlyInAnyOrder(T...)
containsNull()
containsOnly(T...)
doesNotContain(T...)
doesNotContainAnyElementsOf(Iterable<T>)
doesNotContainNull()
doesNotContainsOnlyWitespaces()
etc
Due to ListAssert’s inheritance from multiple interfaces Eclipse's code assistant offers whooping 49 methods once you type contain
after the initial dot to start the method call.
c) API Duplication Due to Negated Versions
In a typical AssertJ assertion class a lot of assertion methods have siblings which provide the negated assertion. For example, AssertJ’s StringAssert
defines these methods and their negated counterparts:
contains(CharSequence...) |
doesNotContain(CharSequence...) |
matches(Pattern) |
doesNotMatchPattern(Pattern) |
isEmpty() |
isNotEmpty() |
startsWith(CharSequence) |
doesNotStartWith(CharSequence) |
etc. |
In effect, this almost doubles API size. Almost, since not all positive assertions have a negative counterpart: For instance, there is no doesNotHaveSize(int)
method in StringAssert
.
In summary: While each method in an AssertJ assertion object is useful in itself, their sheer number is overwhelming. The tragedy of the effort to provide an assertion for every possible use-case is that it will never reach completeness but at the same time is undermining the usefulness of the whole fluent API.
Leaving Shallow Water: Introducing Deep Dive Assertions
Relatively simple refactorings can overcome the issues identified in AssertJs API. I created the fluent assertion library Deep Dive to demonstrate such an alternative.
a) Avoid API Duplication Between Assertion Classes
Deep Dive allows you to go back and forth between different assertion objects, therefore take deep dives into assertion objects for properties of your actual value.
For instance, Deep Dive's assertion object for Strings does not offer a plethora of length-related methods which duplicate the API of Integer assertions. Instead, it offers 1) a simple assertion method length(int)
to state the expected String length (as simple equality assertion) and 2) a no-parameter length()
method which allows you to transition to an Integer assertion object for the String length:
import deepdive.ExpectThat.*;
String s = ...
expectThat(s) // returns an assertion object for the String
.startsWith(″a″) // assertion on the string
.length() // returns an assertion object for the length
.greater(5) // assertion on the string length
.lessEq(100) // assertion on the string length
.back() // diving back up to the string assertions
.contains(″c″); // assertion on the string
b) Avoid API Bloat Due to Method Variations to Cover a Complex Aspect
To avoid API bloat by offering to many method variations for some aspect (e.g., what is contained in a List
) Deep Dive treats them as virtual objects and offers own assertion objects for them. For example, to test if a List
contains specific elements the assertion object offers 1) a one-parameter contains
method to test containment of a single element and 2) a no-parameter contains
method returning an assertion object for the list content.
List<Integer> list = ...
expectThat(list)
.contains(1) // simple contains
.contains() // offers complex assertions for the content
.allOf(2, 3, 5)
.noneOf(9, 10)
.match(n -> n > 100);
c) Avoid API Duplication Due to Negated Versions
Deep Dive assertion objects provide a not()
method which turns the following assertion into its negative counterpart. If Deep Dive offers an assertion, you get the negated version for free:
String s = ...
expectThat(s)
.not().startsWith(″a″)
.length()
.greater(5)
.not().is(7)
.back()
Using the techniques described above, Deep Dive covers core JDK classes with a mere 250Kb. Additionally, it provides support to generate assertion objects for your own classes: Applying fluent assertions to your domain classes will bring your test game to completely new levels.
Have fun using Deep Dive.
Opinions expressed by DZone contributors are their own.
Comments