DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • The Complete Guide to Stream API and Collectors in Java 8
  • Redefining Java Object Equality
  • Automating Cucumber Data Table to Java Object Mapping in Your Cucumber Tests
  • Singleton: 6 Ways To Write and Use in Java Programming

Trending

  • Cloud Security and Privacy: Best Practices to Mitigate the Risks
  • Immutable Secrets Management: A Zero-Trust Approach to Sensitive Data in Containers
  • How to Format Articles for DZone
  • Strategies for Securing E-Commerce Applications
  1. DZone
  2. Coding
  3. Languages
  4. Fluent Assertions Reloaded

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.

By 
Johannes Döbler user avatar
Johannes Döbler
·
Oct. 16, 21 · Tutorial
Likes (5)
Comment
Save
Tweet
Share
5.9K Views

Join the DZone community and get the full member experience.

Join For Free

If 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:

Java
 
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:

Java
 
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 AssertJ StringAssert) 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 in StringAssert, mirroring methods in the String 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:

  1. The fluent API coupled with IDE code completion makes writing such assertions fast and easy.
  2. It produces dense, highly readable test code with an excellent assertion to code ratio.
  3. 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:

Java
 
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:

Java
 
hasSizeBetween(int lowerBoundary, int higherBoundary)
hasSizeGreaterThan(int expected)
hasSizeGreaterThanOrEqualTo(int expected)
hasSizeLessThan(int expected)
hasSizeLessThanOrEqualTo(int expected)


Looking at the API of AssertJ's IntegerAssertwhich provides assertions on int/Integer values we see that the String length assertions quasi duplicate part of the IntegerAssert API:

Java
 
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.

Java
 
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:

Java
 
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.

Java
 
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:

Java
 
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.

Assertion (software development) API Object (computer science) Java (programming language) Testing Data Types

Opinions expressed by DZone contributors are their own.

Related

  • The Complete Guide to Stream API and Collectors in Java 8
  • Redefining Java Object Equality
  • Automating Cucumber Data Table to Java Object Mapping in Your Cucumber Tests
  • Singleton: 6 Ways To Write and Use in Java Programming

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!