v3.24.0
🚫 Deprecated
Core
- Deprecate
ObjectAssert(AtomicReference)
#2795
✨ New Features
- Add Bill of Materials for AssertJ #2748
Core
-
Introduce
IntrospectionStrategy
for recursive comparison and assertion to allow configuring how objects are introspected (thanks @mikybars for the contribution). -
Add
allFieldsSatisfy
andhasNoNullFields
recursive assertions #2211Details
allFieldsSatisfy
verifies that a givenPredicate
is met for all the fields in the field graph of the object under test (i.e., each field is evaluated recursively), but not for the object under test itself.For example, if
actual
is an instance of class A, A has a B field, and B has a C field, thenallFieldsSatisfy
checks A’s B field and B’s C field, and all C’s fields.hasNoNullFields
verifies that none of the fields are null in the field graph of the object under test (i.e., each field is evaluated recursively), but not the object under test itself.It is possible to exclude some fields with any of these methods:
ignoringFields(String... fieldsToIgnore)
- the assertion ignores the specified fields in the object under testignoringFieldsMatchingRegexes(String... regexes)
- the assertion ignores the fields matching the specified regexes in the object under testignoringFieldsOfTypes(Class<?>... typesToIgnore)
- the assertion ignores the object under test fields of the given typesignoringPrimitiveFields()
- avoid running the assertion on primitive fields
Example:
class Author { String name; String email; List<Book> books = new ArrayList<>(); Author(String name, String email) { this.name = name; this.email = email; } } class Book { String title; Author[] authors; Book(String title, Author[] authors) { this.title = title; this.authors = authors; } } Author pramodSadalage = new Author("Pramod Sadalage", "p.sadalage@recursive.test"); Author martinFowler = new Author("Martin Fowler", "m.fowler@recursive.test"); Author kentBeck = new Author("Kent Beck", "k.beck@recursive.test"); Book noSqlDistilled = new Book("NoSql Distilled", new Author[] {pramodSadalage, martinFowler}); pramodSadalage.books.add(noSqlDistilled); martinFowler.books.add(noSqlDistilled); Book refactoring = new Book("Refactoring", new Author[] {martinFowler, kentBeck}); martinFowler.books.add(refactoring); kentBeck.books.add(refactoring); // assertion succeeds assertThat(pramodSadalage).usingRecursiveAssertion() .allFieldsSatisfy(field -> field != null); // best rewritten with `hasNoNullFields()` assertThat(pramodSadalage).usingRecursiveAssertion() .hasNoNullFields();
-
Add
withEqualsForFieldsMatchingRegexes
to the recursive comparison #2711Details
Allows registering a
BiPredicate
to compare fields whose location matches the given regexes.A typical usage consists of comparing double/float fields with a given precision.
The fields are evaluated from the root object. For example, if
Foo
has aBar
field and both have anid
field, one can register aBiPredicate
forFoo
andBar
id
by calling:withEqualsForFieldsMatchingRegexes(idBiPredicate, ".*id")
or:
withEqualsForFieldsMatchingRegexes(idBiPredicate, "foo.*id")
Example:
class TolkienCharacter { String name; double height; double weight; } TolkienCharacter frodo = new TolkienCharacter("Frodo", 1.2, 40); TolkienCharacter tallerFrodo = new TolkienCharacter("Frodo", 1.3, 40.5); BiPredicate<Double, Double> closeEnough = (d1, d2) -> Math.abs(d1 - d2) <= 0.5; // assertion succeeds as both weight and height diff is less than 0.5 assertThat(frodo).usingRecursiveComparison() .withEqualsForFieldsMatchingRegexes(closeEnough, ".eight") .isEqualTo(tallerFrodo);
-
Add
comparingOnlyFieldsOfTypes
to the recursive comparison #2794Details
Makes the recursive comparison to only compare given actual fields of the specified types and their subfields (no other fields will be compared).
Specifying a field of type will make all its subfields to be compared. For example, specifying the
Person
type will lead to comparingPerson.name
,Person.address
and all otherPerson
fields. In caseactual
's field is null,expected
's field type will be checked to match one of the given types (we assumeactual
andexpected
fields have the same type).comparingOnlyFieldsOfTypes
can be combined withcomparingOnlyFields(String...)
to compare fields of the given types or names (union of both sets of fields).comparingOnlyFieldsOfTypes
can be also combined with ignoring fields or compare only fields by name methods to restrict further the fields actually compared, the resultingcompared fields = {specified compared fields of types} - {specified ignored fields}
.For example, if the specified compared fields of types are
{String.class, Integer.class, Double.class}
, when there are fieldsString foo
,Integer baz
andDouble bar
and the ignored fields ={"bar"}
set withignoringFields(String...)
that will removebar
field from comparison, then only{foo, baz}
fields will be compared.Example:
class Person { String name; double height; Home home = new Home(); } class Home { Address address = new Address(); } class Address { int number; String street; } Person sherlock = new Person("Sherlock", 1.80); sherlock.home.address.street = "Baker Street"; sherlock.home.address.number = 221; Person moriarty = new Person("Moriarty", 1.80); moriarty.home.address.street = "Butcher Street"; moriarty.home.address.number = 221; // assertion succeeds as it only compared fields height and home.address.number since their types match compared types assertThat(sherlock).usingRecursiveComparison() .comparingOnlyFieldsOfTypes(Integer.class, Double.class) .isEqualTo(Moriarty); // assertion fails as home.address.street fields differ (Home fields and its subfields were compared) assertThat(sherlock).usingRecursiveComparison() .comparingOnlyFieldsOfTypes(Home.class) .isEqualTo(moriarty);
-
Add
isIn
/isNotIn
assertions to the recursive comparison #2794Details
isIn
verifies thatactual
is present in the given iterable/array, whileisNotIn
verifies thatactual
is not present in the given iterable/array. Both compare values with the recursive comparison.class Person { String name; double height; Home home = new Home(); } class Home { Address address = new Address(); } class Address { int number; String street; } Person sherlock = new Person("Sherlock", 1.80); sherlock.home.ownedSince = new Date(123); sherlock.home.address.street = "Baker Street"; sherlock.home.address.number = 221; Person sherlock2 = new Person("Sherlock", 1.80); sherlock2.home.ownedSince = new Date(123); sherlock2.home.address.street = "Baker Street"; sherlock2.home.address.number = 221; Person watson = new Person("Watson", 1.70); watson.home.ownedSince = new Date(123); watson.home.address.street = "Baker Street"; watson.home.address.number = 221; Person moriarty = new Person("Moriarty", 1.80); moriarty.home.ownedSince = new Date(123); moriarty.home.address.street = "Butcher Street"; moriarty.home.address.number = 221; // assertion succeeds as sherlock and sherlock2 data are the same but not for watson and moriarty assertThat(sherlock).usingRecursiveComparison() .isIn(sherlock2, Moriarty) .isNotIn(watson, moriarty);
-
Add
RecursiveComparator
that uses the recursive comparison for any assertions -
Add
satisfiesOnlyOnce
to iterable, array, and atomic array assertions #2691Details
Verifies that there is exactly one element in the iterable/array under test that satisfies the given
Consumer
.Example:
List<String> starWarsCharacterNames = List.of("Luke", "Leia", "Yoda"); // these assertions succeed: assertThat(starWarsCharacterNames).satisfiesOnlyOnce(name -> assertThat(name).contains("Y")) // matches only "Yoda" .satisfiesOnlyOnce(name -> assertThat(name).contains("Lu")) // matches only "Luke" .satisfiesOnlyOnce(name -> assertThat(name).contains("Le")); // matches only "Leia" // this assertion fails because the requirements are satisfied two times assertThat(starWarsCharacterNames).satisfiesOnlyOnce(name -> assertThat(name).contains("a")); // matches "Leia" and "Yoda" // this assertion fails because no element contains "Han" assertThat(starWarsCharacterNames).satisfiesOnlyOnce(name -> assertThat(name).contains("Han"));
-
Add
isAssignableTo
toClass
assertions #2611Details
Verifies that the
Class
under test is assignable to the given one.Example:
class Jedi {} class HumanJedi extends Jedi {} // this assertion succeeds assertThat(HumanJedi.class).isAssignableTo(Jedi.class); // this assertion fails assertThat(Jedi.class).isAssignableTo(HumanJedi.class);
-
Add
withThrowableThat
to easeFuture
/CompletableFuture
throwable assertions #2704Details
Returns a
ThrowableAssertAlternative
to chain assertions on the underlying throwable forFuture
/CompletableFuture
assertions.Example:
CompletableFuture<Void> completableFuture = new CompletableFuture<>(); completableFuture.completeExceptionally(new RuntimeException("boom!")); then(completableFuture).failsWithin(Duration.ofMillis(1)) .withThrowableThat() .isInstanceOf(ExecutionException.class) .withMessageContaining("boom!");
-
Add
matchesSatisfying
for checking pattern followed by assertion on matcher groups #2723Details
Verifies that the actual
CharSequence
matches the given regular expression pattern, then accepts the givenConsumer<Matcher>
to do further verification on the matcher.Example:
assertThat("Frodo").matchesSatisfying("..(o.o)", matcher -> assertThat(matcher.group(1)).isEqualTo("odo")); // same assertion but with a Pattern Pattern pattern = Pattern.compile("..(o.o)"); assertThat("Frodo").matchesSatisfying(pattern, matcher -> assertThat(matcher.group(1)).isEqualTo("odo"));
-
Add
NestableCondition
#2726Details
NestableCondition::nestable
is a building block defining a more precise condition on complex objects. It allows the creation of readable assertions and produces nicer assertion error messages.Example:
import static org.assertj.core.condition.NestableCondition.nestable; import static org.assertj.core.condition.VerboseCondition.verboseCondition; class Customer { final String name; final Address address; Customer(String name, Address address) { this.name = name; this.address = address; } } class Address { final String firstLine; final String postcode; Address(String firstLine, String postcode) { this.firstLine = firstLine; this.postcode = postcode; } } static Condition<Customer> name(String expected) { return new Condition<>( it -> expected.equals(it.name), "name: " + expected ); } static Condition<Customer> customer(Condition<Customer>... conditions) { return nestable("person", conditions); } static Condition<Address> firstLine(String expected) { return new Condition<>( it -> expected.equals(it.firstLine), "first line: " + expected ); } static Condition<Address> postcode(String expected) { return new Condition<>( it -> expected.equals(it.postcode), "postcode: " + expected ); } static Condition<Customer> address(Condition<Address>... conditions) { return nestable( "address", customer -> customer.address, conditions ); }
An assertion written like:
assertThat(customer).is( customer( name("John"), address( firstLine("3"), postcode("KM3 8SP") ) ) );
will produce an easy-to-read assertion error:
Expecting actual: org.assertj.core.condition.Customer@27ff5d15 to be: [✗] person:[ [✓] name: John, [✗] address:[ [✗] first line: 3, [✓] postcode: KM3 8SP ] ]
For an even better assertion error, see
VerboseCondition
. -
Add
isAlphabetic
toCharSequence
assertions #2731Details
Verifies that the actual
CharSequence
is alphabetic by checking it against the\p{Alpha}+
regex pattern POSIX character classes (US-ASCII only).Example:
// assertions will pass assertThat("lego").isAlphabetic(); assertThat("a").isAlphabetic(); assertThat("Lego").isAlphabetic(); // assertions will fail assertThat("1").isAlphabetic(); assertThat(" ").isAlphabetic(); assertThat("").isAlphabetic(); assertThat("L3go").isAlphabetic();
-
Add
isAlphanumeric
toCharSequence
assertions #2731Details
Verifies that the actual
CharSequence
is alphanumeric by checking it against the\p{Alnum}+
regex pattern POSIX character classes (US-ASCII only).Example:
// assertions will pass assertThat("lego").isAlphanumeric(); assertThat("a1").isAlphanumeric(); assertThat("L3go").isAlphanumeric(); // assertions will fail assertThat("!").isAlphanumeric(); assertThat("").isAlphanumeric(); assertThat(" ").isAlphanumeric(); assertThat("L3go!").isAlphanumeric();
-
Add
isASCII
toCharSequence
assertions #2731Details
Verifies that the actual
CharSequence
is ASCII by checking it against the\p{ASCII}+
regex pattern POSIX character classes (US-ASCII only).Example:
// assertions will pass assertThat("lego").isASCII(); assertThat("a1").isASCII(); assertThat("L3go").isASCII(); // assertions will fail assertThat("").isASCII(); assertThat("♪").isASCII(); assertThat("⌃").isASCII(); assertThat("L3go123⌃00abc").isASCII();
-
Add
isHexadecimal
toCharSequence
assertions #2731Details
Verifies that the actual
CharSequence
is hexadecimal by checking it against the\p{XDigit}+
regex pattern POSIX character classes (US-ASCII only).Example:
// assertions will pass assertThat("A").isHexadecimal(); assertThat("2").isHexadecimal(); // assertions will fail assertThat("!").isHexadecimal(); assertThat("").isHexadecimal(); assertThat(" ").isHexadecimal(); assertThat("Z").isHexadecimal(); assertThat("L3go!").isHexadecimal();
-
Add
isPrintable
toCharSequence
assertions #2731Details
Verifies that the actual
CharSequence
is printable by checking it against the\p{Print}+
regex pattern POSIX character classes (US-ASCII only).Example:
// assertions will pass assertThat("2").isPrintable(); assertThat("a").isPrintable(); assertThat("~").isPrintable(); assertThat("").isPrintable(); // assertions will fail assertThat("\t").isPrintable(); assertThat("§").isPrintable(); assertThat("©").isPrintable(); assertThat("\n").isPrintable();
-
Add
isVisible
toCharSequence
assertions #2731Details
Verifies that the actual
CharSequence
is visible by checking it against the\p{Graph}+
regex pattern POSIX character classes (US-ASCII only).Example:
// assertions will pass assertThat("2").isVisible(); assertThat("a").isVisible(); assertThat(".").isVisible(); // assertions will fail assertThat("\t").isVisible(); assertThat("\n").isVisible(); assertThat("").isVisible(); assertThat(" ").isVisible();
-
Add
isUnmodifiable
toMap
assertions #2458Details
Verifies that the map under test is unmodifiable, i.e. throws an
UnsupportedOperationException
with any attempt to modify the map.Example:
// assertions succeeds assertThat(Collections.unmodifiableMap(new HashMap<>())).isUnmodifiable(); // assertions fails assertThat(new HashMap<>()).isUnmodifiable();
-
Add
isExecutable
toFile
assertions #2829Details
Verifies that the file under test is executable.
Example:
File tmpFile = java.nio.file.Files.createTempFile("executable_file", ".sh").toFile(); tmpFile.setExecutable(true); // assertions succeeds assertThat(tmpFile).isExecutable(); tmpFile.setExecutable(false); // assertions fails assertThat(tmpFile).isExecutable();
-
Add
hasMessageMatching(Pattern)
toThrowable
assertions #2853Details
Verifies that the message of the
Throwable
under test matches the givenPattern
regular expression.Example:
Throwable throwable = new IllegalArgumentException("wrong amount 123"); // assertion succeeds assertThat(throwable).hasMessageMatching(Pattern.compile("wrong amount [0-9]*")); // assertion fails assertThat(throwable).hasMessageMatching(Pattern.compile("wrong amount [0-9]* euros"));
Guava
- AssertJ Guava is now part of the AssertJ multimodule and follows the same versioning
🐛 Bug Fixes
Core
- Avoid reflection in recursive comparison for types in
sun.*
packages #2891 - Honor custom type comparators with
returns
anddoesNotReturn
#2725 - Fix
ignoringOverriddenEqualsForFieldsMatchingRegexes
that was checking field class instead of the field path - Add support for soft assertions when calling
isThrownBy
#2699 - Fix
isCloseTo
Javadoc example forDate
assertions #2890
⚡ Improvements
Core
- Generalize custom format creation to support any
CharSequence
#2735 - Improve
containsExactly
error message by printing the indices where a mismatch occurred #2638 - Throw
org.opentest4j.AssertionFailedError
forcontainsExactly
to get theactual
vs.expected
diff in the IDE #2697 - Fix some typos and grammar in project documentation #2656
- Fix typo in docs for
assertIsEqualToNormalizingNewlines
#2677 - Fix javadoc for
within(long, TemporalUnit)
#2801 - Let
CaseInsensitiveStringComparator
delegate tocompareToIgnoreCase
#2663 - Use actual
Map
type when invoking copying constructor #2710 - Improve
isBetween
assertion performance by only building the error message when needed #2884 - Avoid a copy in
assertContains
whenactual
is already aCollection
#2220 - Repair code style issues in
org.assertj.core.internal.Dates
#2763 - Reduce
RealNumbers
constructors' visibility toprotected
#2772 - Use
getParameterCount
rather thangetParameterTypes().length
#2813 - Return cache in
setup-java
action #2802 - Fix test results summary #2803
- Remove
public
modifier to fix some sonar violations - Clean up unused imports
- Migrate
MatcherShouldMatch_create_Test
andStandardRepresentation_toStringOf_AtomicReferences_Test
to JUnit Jupiter - Require Java 17 in Gitpod configuration
- Swap out deprecated-for-removal methods in
BDDAssertions_then_Test.java
#2644 - Fix 95 warnings produced on JDK-17 with
-Xlint:all
#2648 - Fix spurious Future tests failure on Windows runners #2649
- Fix CI allowing
add-label
to fail inbinary-compatibility.yml
#2667 - Improve CI output #2669
- Reset global state more aggressively in tests #2670
- Fix incomplete test for
given(Duration)
inBDDAssumptionsTest
#2686 - Fix some test flakiness #2657
- Fix some additional compilation warnings and deprecated JDK API usages
- Remove CodeQL workflow
- Reuse spies in
Path
tests #2826 - Reuse mocks and spies in
File
tests #2827 - Remove unnecessary test for
MapEntry::hashCode
- Improve JaCoCo configuration
- Use
oracle-actions/setup-java
for cross-version workflow #2658 - Some more Turkish locale cleanup
- Use specific labels for Dependabot
- Switch license URL to HTTPS
- Move assumptions related integration tests into separate modules #2737
- Move OSGi related integration tests into separate modules #2742
- Move performance tests into separate modules #2743
- Add integration test infrastructure for Groovy #2762
- Add integration test infrastructure for Kotlin #2788
- Add editorconfig for Kotlin files
- Fix locale handling for ignoring case assertions #2690
- Enforce test only dependencies in test modules
- Move japicmp common configuration to the parent POM #2875
- Allow Java 20 with Groovy tests #2816
- Add Java 21, skip JaCoCo on cross-version workflow #2876
🔨 Dependency Upgrades
- Upgrade to Bnd 6.4.0 #2860
- Upgrade to Byte Buddy 1.12.20 #2880
- Upgrade to EqualsVerifier 3.12.3 #2877
- Upgrade to GMavenPlus Plugin 2.1.0 #2818
- Upgrade to Groovy 4.0.7 #2889
- Upgrade to Hibernate Core 6.1.6.Final #2869
- Upgrade to Jackson Databind 2.14.1 #2854
- Upgrade to Japicmp Maven Plugin 0.17.1 #2847
- Upgrade to JUnit BOM 5.9.1 #2785
- Upgrade to JUnit Pioneer 1.9.1 #2698
- Upgrade to Maven 3.8.7
- Upgrade to Maven Install Plugin 3.1.0 #2851
- Upgrade to Maven JAR Plugin 3.3.0 #2782
- Upgrade to Maven Javadoc Plugin 3.4.1 #2738
- Upgrade to Maven Project Info Reports Plugin #2741
- Upgrade to Maven Site Plugin #2730
- Upgrade to Mockito BOM #2893
- Upgrade to Kotlin 1.8.0 #2892
- Upgrade to OSGi System Bundle #2868
- Upgrade to PITest 1.0.3 #2866
- Upgrade to PITest Maven 1.10.4 #2898
- Upgrade to SpotBugs Maven Plugin 4.7.3.0 #2834
- Upgrade to Spring Core 5.3.24
- Upgrade to TestNG 7.7.1 #2873
- Upgrade to Versions Maven Plugin 2.14.2 #2888
❤️ Contributors
Thanks to all the contributors who worked on this release:
@ascopes @nstdio @Bananeweizen @mbaechler @StefanBratanov @stevenschlansker @jessicant @MartinWitt @nith2001 @WojciechZankowski @drakulavich @ChrisZoet @DirkToewe @3flex @opwvhk @alex859 @Giovds @NeshkaD @snuyanzin @rostIvan @remika @micd
Special thanks to those who contributed many internal improvements to our test suite, much appreciated!