Coding Confidently with Java Assertions
Assert statement are crucial in providing a clear status of your tests, making them self-validating and easily understandable as default boolean expression: pass or fail.
In Java programming language, there are several libraries available for writing assertion statements. You can use simple assertions from popular testing frameworks like JUnit or TestNG, or explore more advanced options like Hamcrest. Additionally, you can opt for fluent assertions, which offer greater readability and a natural language approach. For this approach, you can use the AssertJ or Truth library.
This article will briefly describe each library, accompanied by simple examples. Then, we will show some special cases, focusing on JUnit, AssertJ, and Hamcrest.
Brief Library Descriptions
JUnit
We would start with a trendy testing framework. This article will focus more on JUnit 5 than JUnit 4 since it is becoming popular in programming. However, it’s worth mentioning that JUnit 4 is still widely used for all kinds of applications and programs.
Let's kick things off with a straightforward example of how to use JUnit for testing. In this case, we use the assertEquals method, which checks whether two variables are equal.
Alternatively, we can enhance code readability by using static import for the JUnit's assertion methods:
We can also add a custom error message displayed when an assertion fails.
JUnit provides a range of other public methods available to be used with the followind assert keywords: assertAll(), assertArrayEquals(), assertTrue(), assertFalse(), assertNull(), assertNotNull(), assertNotEquals(), and many more. These assert statement are intuitive to use within an Integrated Development Environment (IDE). However, when viewed outside the IDE, developers must remember which argument represents the expected value and which represents the actual value.
Refer to the comprehensive documentation for a list of all available assertion check methods or potential question.
TestNG
Let's explore another testing framework similar to JUnit in terms of assertion syntax. TestNG provides a familiar way to assertions performance. In the following example, we'll see how the previous test case would look when using TestNG in your program:
You'll notice that the method structure remains similar to JUnit, but instead of the Assertions class, we use Assert keyword in TestNG. Additionally, you can opt for a static import and include a custom message for better clarity.
TestNG also offers a variety of other assert method invocation like “assertNotEquals()”, “assertNull()”, “assertTrue()”, “assertFalse()”, “assertNotNull()”, and more.
Notably, TestNG supports soft and hard assertions, which we will discuss later.
For more details and help with TestNG, check out the documentation. Make sure you choose the right library version when visiting the documentation.
Hamcrest
While still popular, it may not be as widely used as it once was. What sets Hamcrest apart is its extensive collection of matchers, allowing for complex assertions by combining matchers with logical operators. You can use Hamcrest with both JUnit and TestNG for your testing needs.
However, it's worth noting that the Hamcrest community has become less active over time. The latest version of Hamcrest was released 2019 roughly three years ago. This suggests that Hamcrest may not be actively maintained, potentially resulting in compatibility issues with newer Java versions.
Let's take a look at a basic example of using Hamcrest with JUnit:
As you can see, Hamcrest introduces a different syntax with matcher conditions. We'll explore more examples of Hamcrest's capabilities later in the article.
For in-depth information and documentation on Hamcrest, you can visit their website.
AssertJ
Let’s move to the widely used library for fluent assertions, AssertJ. AssertJ stands out for its extensive range of assertions designed for various data types. It provides special modules tailored for different purposes, including Core, Guava, Joda Time, Neo4J, DB, and Swing modules.
What makes AssertJ particularly appealing is its clear and readable assertion syntax. It's easy to discern which value is expected and which one represents the actual result.
Let's examine a simple example of using AssertJ with JUnit:
Similarly to the previous libraries, you can simplify the production code base with static import for AssertJ.
For comprehensive documentation and further information on AssertJ, you can visit their website.
Truth
Let's explore another library known for its fluent assertions, Truth. Developed by Google's Guava team, Truth draws inspiration from AssertJ, resulting in a similar syntax. However, it's worth noting that Truth offers a more limited set of assertions than AssertJ.
Here's a straightforward example of using Truth with JUnit.
As you can see, the method structure in Truth looks similar to AssertJ.
For further details and documentation on Truth, visit their website.
Different Cases
This section will delve into various scenarios and explore how different assertion libraries can be applied effectively. We will examine cases ranging from comparing primitive types and handling collections to working with custom objects. Additionally, we will explore scenarios like multiplication, dealing with normal exceptions, defining timeouts, and creating custom assertions.
It's crucial to have a solid grasp of these cases and understand how to employ the right assertion library for each situation. While the library choice depends on your project's specific requirements and personal preferences, this section will equip you with the knowledge to make informed decisions.
Simple Comparison Assertions
Let's explore basic comparison assertions for various testing libraries. These assertions help you verify if values are equal or not. They apply to different data types, not only primitive but also more complex ones. These bare comparison assertions provide a foundation for more advanced testing scenarios. It's important to note that while these fundamental comparisons are helpful, they should not be overused. Consider using more specific methods available within these libraries to enhance code clarity.
JUnit/TestNG
JUnit and TestNG share a similar syntax for these comparisons. Here's how you can check for equality:
To check for inequality, use a different method with the same set of arguments:
Hamcrest
Hamcrest has its unique approach. When comparing for equality, use the equalTo() method:
To check for inequality, use the not() matcher, which negates the equalTo() matcher:
AssertJ/Truth
AssertJ and Truth libraries offer similar syntax for these cases. When checking for equality, you can use the isEqualTo() method:
To verify inequality, use the isNotEqualTo() method:
Boolean Assertions
This section'll focus on boolean assertions, a specific type of primitive value. When dealing with boolean conditions, libraries can offer dedicated methods which enhance code clarity and readability.
JUnit/ TestNG
For this case, JUnit and TesNg also have similar syntax.
To check whether a value is true, use assertTrue():
To check whether a value is false, use assertFalse():
Hamcrest
Hamcrest takes a slightly different approach, using the is() matcher for both checks.
You can use is(true) to check whether a value is true.
You can use is(false) to check whether a value is false.
AssertJ/Truth
AssertJ and Truth libraries provide dedicated isTrue() and isFalse() methods for boolean assertions.
Here is an example with isTrue() checking if a value is true.
With isFalse() we can check if a value is false.
Number Assertions
In this subsection, we’ll look at the number assertions. It's worth noting that not all of these libraries have dedicated methods for number assertions. The assertions we discuss apply to a range of numeric types, including integers (int), floating-point numbers (double and float), and long integers (long). Some also extend their support to the BigDecimal data type. Our examples will primarily concentrate on integers for simplicity.
JUnit/TestNG
Neither JUnit nor TestNG offers dedicated methods for number assertions. Instead, you have to rely on more general assertion methods. For instance, you can utilize assertTrue() to determine whether a value is greater than another.
While it's possible to write unit tests using these general methods, using dedicated number assertion methods often provides greater convenience.
Hamcrest
Hamcrest, on the other hand, provides dedicated matchers for number assertions.
You can use these matchers to check if a number is greater or less than an expected value.
Moreover, you can use Hamcrest to check if a number is approximately equal to an expected value, a feature that works for double and BigDecimal data types.
AssertJ
AssertJ stands out with its extensive range of dedicated methods for number assertions. This library provides various methods to assert different aspects of numbers.
Truth
Truth offers dedicated methods for number assertions. These methods cover various scenarios, and not all are implemented for all number types, such as integers:
Now, look at the examples of doubles.
To explore more, you can refer to Javadoc or the library's source code:
Similar source code is available for Float (FloatSubject) and BigDecimal (BigDecimalSubject) assertions.
This revised section provides a clearer and more refined explanation of number assertions in Java, focusing on each library's unique features.
String Assertions
Let's focus on string assertions. Similar to number assertions, only some libraries have dedicated methods. However, libraries can contain various dedicated methods, and showcasing some of them is only possible.
JUnit/TestNG
We have the same case as for number assertions. You must use general assertions, as there are no dedicated string assertions for JUnit or TestNG. For example, assertFalse() can check if a string is not blank.
Hamcrest
Hamcrest provides numerous matchers for string assertions. Here are some examples:
AssertJ
AssertJ also has many dedicated assert methods related to string. There are so many methods, so we will group them and mention only a few. You can look in Javadoc for more.
First, let’s start by checking whether a string is empty or blank.
We can also check the string’s size.
We also have methods checking if your string contains specific values. You can look at the whole string or only the beginning or ending.
You can also check if your sting doesn’t contain specific values.
You can also check patterns and regex. Some methods have defined regex, including checking lowercase or mixed.
Truth
Truth also has string asserts, but only so many as in AssertJ.
Collection and Arrays Assertions
We'll categorize collections into distinct groups for clarity. The Iterable interface extends to List, Queue, and Set, while the Map interface stands independently. Though not directly related to the mentioned groups, we'll also consider arrays.
You can look at the diagram to see the example class. There are not all of them, only the most used ones.
Test libraries may feature methods exclusive to collections implementing the Iterable interface. However, some methods can validate both Iterable and Map objects. The article provides a few examples of usage; refer to the Javadoc. Switch the map to a set (using the method entrySet()) if you prefer assertions dedicated only to Iterable objects.
JUnit
JUnit offers specific methods for comparing arrays and iterable objects. For arrays, you can use assertArrayEquals(). For Iterable, we have assertIterableEquals().
TestNG
TestNG provides dedicated assertions for collections and arrays.
Hamcrest
Hamcrest offers numerous matchers related to arrays and collections. Refer to the javadoc for details.
Let’s start with arrays.
Let’s now check the matchers for Collection.
We have also the matchers dedicated for Iterables.
Let’s focus also on dedicated matchers for maps.
AssertJ
AssertJ has a lot of dedicated assertions for arrays and collections. The following examples demonstrate some of them; look at the javadoc for more details.
First, look at the assertions for Iterable or arrays. You can find documentation on https://javadoc.io/static/org.assertj/assertj-core/3.24.2/org/assertj/core/api/AbstractIterableAssert.html and https://javadoc.io/static/org.assertj/assertj-core/3.24.2/org/assertj/core/api/AbstractObjectArrayAssert.html.
We would like to use arrays and list assertions in the examples below.
We also have assertions that check whether elements match predicates.
AssertJ has also many assertions which check if an array/iterable contains certain values
We can also check if elements satisfy some conditions, for example, if they are duplicates.
We can also check if an array/iterable is sorted
AssertJ has also dedicated assertions for maps. You can find documentation here.
First, let’s start with simple assertions. We can check if the map is empty or null.
We have assertions checking the map’s size. You can check also if the size is in range or is greater or less than expected values.
We can also check whether a map contains expected entries, keys, or values. We can also extract value or entry.
Truth
Truth offers dedicated assertions for arrays, iterable objects, and maps.
Let's start with specialized assertions concerning arrays. It's worth noting that the available assertions can vary based on the element types. For instance, you can convert an int array to a list subject, but this is impossible for a double array.
There are a lot of dedicated assertions related to iterable objects.
Truth has also dedicated assertions related to the maps.
like this
Custom Object/Reference Type Assertions
When dealing with custom objects and reference types, it's essential to understand assertions tailored to these types. One approach is to override the compareTo() method, impacting general assertions.
For our examples, we'll use the CharacterDoctorWho record:
Creating an instance is straightforward:
We'll also use the Cat class with the Lombok annotations for convenient getters, setters, a builder and a constructor:
Initialization is also simple:
JUnit/TestNG
JUnit and TestNG provide two dedicated assertions related to objects, allowing you to verify whether an object is null or not:
Hamcrest
Hamcrest offers various methods for object-related assertions. Let's explore some basic ones:
Additionally, Hamcrest allows comparing objects while ignoring specified fields:
You can also examine if an object contains a specific field and check its value:
AssertJ
AssertJ provides numerous dedicated assertions for objects.
Let's start with some simple assertions:
AssertJ also provides methods to ignore specific fields during object comparison:
You can also check fields or properties. You can make sure that they are all null or that none of them is null. You can check if there is no null field or property. There is also a method for extracting a specific field or checking if a specific field exists.
Truth
Truth offers a few dedicated assertions for objects. You can check if an object is null or not null. These assertions include checking if an object is null or not null, if objects are the same instance, or if an object is an instance of a specific class.
Exceptions Assertions
JUnit and TestNG stand out among test libraries by offering dedicated assertions tailored for exception handling. In contrast, other test libraries lack specific assertions designed explicitly for dealing with exceptions.
JUnit/TestNG
Both JUnit 5 and TestNG provide an assertion called assertThrows() to verify the thrown exceptions. This assertion is valuable for ensuring that specific exceptions are thrown during the execution of your code.
Multiply Assertions/Soft Assertions
When testing, there are instances where we need to verify multiple aspects in a single test. This is frequently observed in lengthy tests such as end-to-end (e2e) tests. For example, suppose we have a Cat object, and we wish to confirm that various information is accurate.
Let's take a look at our Cat class:
Now, here are the steps for our test:
- Check the name
- Check the breedCat
- Check the colourFur
- Check the patternFur
Suppose the cat we're looking at has the wrong name and patternFur. If we use hard assertions (the regular ones), the test will stop when it finds the first problem. In our case, it would tell us about the wrong name but miss the wrong patternFur.
Here's where soft assertions come in handy. They let us keep checking multiple things even if we find a problem. This gives us a better overall view of what might be wrong in a single test.
When using assertions, sticking to one library is important. Mixing different assertion libraries is not supported. Choose and use the one that fits your needs.
JUnit
JUnit provides the assertAll() method for soft assertions. You can pass multiple assertions as arguments, and it will report all failures.
However, it's crucial to use only JUnit assertions inside assertAll(). Assertions from other test libraries won't work correctly.
In this example, you have 3 failed assertions and you should be informed about all issues.
TestNG
TestNG also supports soft assertions through the SoftAssert class. Create a SoftAssert object, execute assertions on this object, and finally, use the assertAll() method to check all assertions at the end of the test.
For the above example, three failed assertions exist.
AssertJ
AssertJ also has implemented soft assertions. Similar to TestNG, we have to use the SoftAssrtions object.
Three failed assertions are included in the test report.
Hamcrest/Truth
Hamcrest and Truth do not provide a built-in mechanism for soft assertions. To implement soft assertions, you may need to manually catch and accumulate assertion errors.
Stacked Assertions
Sometimes, you may need to verify multiple conditions for a single object, particularly in longer end-to-end (e2e) tests. Stacked assertions can be beneficial in such scenarios, enhancing the clarity of your tests. It's important not to confuse stacked assertions with soft assertions; they address different aspects of testing.
Let's explore examples in two test libraries, AssertJ and Hamcrest, as they are the ones that support stacked assertions. Other libraries do not currently include this feature.
AssertJ
AssertJ has a very simple syntax for stacked assertions. You just have to add the next assertions to the previous one. All assertions should be satisfied. Also, they are hard assertions, so after the first failed tests are terminated you are only noticed about the first failure.
Let’s look at the example. Used assertions are from “Collections and Arrays Assertions”
Hamcrest
Hamcrest provides logical matchers, expanding the possibilities for creating stacked assertions when compared to AssertJ. Using nested expressions by combining multiple logical matchers makes it even more flexible.
Logical matchers:
- not() works as logical operation NOT
- anyOf() works as logical operation OR
- allOf() works as logical operation AND
- either().or() works as a logical operation OR for two matches
- both().and() works as a logical operation AND for two matches
Now, let's explore one-argument and two-argument logical matchers.
There we have logical matchers with more possible arguments. There is also a “contains()” matcher for iterable. It works similarly to allOf(), but only for the iterable subjects.
Custom assertions
Sometimes, implemented assertion methods may not cover specific cases. For such scenarios, you can create custom assertions.
JUnit/TestNG
In JUnit and TestNG, it's not possible to create custom assertions directly. You are limited to using the implemented ones provided by the frameworks.
Hamcrest
Hamcrest allows the creation of custom assertions by defining a class that extends TypeSafeMatcher<T>, where T is the type of the object you want to verify. Here's an example for a custom assertion checking if an Integer is a magic number:
With this custom assertion, you can now use it in your tests:
AssertJ
AssertJ also provides the possibility to create custom assertions. You need to create a class that extends AbstractAssert<YourClass, T>, where T is the type of object you want to verify. Alternatively, you can extend a subclass of AbstractAssert.
In this example, we are verifying the Cat class and its parameter name. For more explanations, you can refer to the official docs.
Now, let’s explore the usage of our assertion.
The second test will fail with a custom message.
Truth
In Truth, you can create custom assertions by extending the Subject class. Inspired by the documentation available here: https://truth.dev/extension, you can find more information on creating private methods.
Here is an example of how to use the created assertions:
Summary
This article dives into the world of assertions, covering various testing aspects. While it provides a comprehensive overview, it's crucial to note that the topic is extensive, and only some assertion types are discussed in detail. The range of assertions includes basics and more complex elements like objects and collections, along with unexplored types such as dates, databases, or paths, which can be explored in libraries’ documentation.
The article highlights their unique features, highlighting popular testing frameworks like JUnit, TestNG, Hamcrest, AssertJ, and Truth. Each library has its pros and cons. When choosing a library for testing purposes, considering all aspects is vital. This article aims to guide developers through the diverse realm of assertion libraries, helping them make informed decisions based on their specific testing needs.