๐งฉ Binary Compatibility
The release is binary compatible with the previous minor version.
๐ซ Deprecated
Core
- Deprecate
ObjectAssert(AtomicReference)#2795
โจ New Features
- Add Bill of Materials for AssertJ #2748
Core
-
Introduce
IntrospectionStrategyfor recursive comparison and assertion to allow configuring how objects are introspected -
Add
allFieldsSatisfyandhasNoNullFieldsrecursive assertions #2211Details
allFieldsSatisfyverifies that a givenPredicateis 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
actualis an instance of class A, A has a B field, and B has a C field, thenallFieldsSatisfychecks Aโs B field and Bโs C field, and all Cโs fields.hasNoNullFieldsverifies 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
withEqualsForFieldsMatchingRegexesto the recursive comparison #2711Details
Allows registering a
BiPredicateto 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
Foohas aBarfield and both have anidfield, one can register aBiPredicateforFooandBaridby 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
comparingOnlyFieldsOfTypesto 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
Persontype will lead to comparingPerson.name,Person.addressand all otherPersonfields. In caseactual's field is null,expected's field type will be checked to match one of the given types (we assumeactualandexpectedfields have the same type).comparingOnlyFieldsOfTypescan be combined withcomparingOnlyFields(String...)to compare fields of the given types or names (union of both sets of fields).comparingOnlyFieldsOfTypescan 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 bazandDouble barand the ignored fields ={"bar"}set withignoringFields(String...)that will removebarfield 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/isNotInassertions to the recursive comparison #2794Details
isInverifies thatactualis present in the given iterable/array, whileisNotInverifies thatactualis 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
RecursiveComparatorthat uses the recursive comparison for any assertions -
Add
satisfiesOnlyOnceto 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
isAssignableTotoClassassertions #2611Details
Verifies that the
Classunder 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
withThrowableThatto easeFuture/CompletableFuturethrowable assertions #2704Details
Returns a
ThrowableAssertAlternativeto chain assertions on the underlying throwable forFuture/CompletableFutureassertions.Example:
CompletableFuture<Void> completableFuture = new CompletableFuture<>(); completableFuture.completeExceptionally(new RuntimeException("boom!")); then(completableFuture).failsWithin(Duration.ofMillis(1)) .withThrowableThat() .isInstanceOf(ExecutionException.class) .withMessageContaining("boom!");
-
Add
matchesSatisfyingfor checking pattern followed by assertion on matcher groups #2723Details
Verifies that the actual
CharSequencematches 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::nestableis 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
isAlphabetictoCharSequenceassertions #2731Details
Verifies that the actual
CharSequenceis 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
isAlphanumerictoCharSequenceassertions #2731Details
Verifies that the actual
CharSequenceis 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
isASCIItoCharSequenceassertions #2731Details
Verifies that the actual
CharSequenceis 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
isHexadecimaltoCharSequenceassertions #2731Details
Verifies that the actual
CharSequenceis 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
isPrintabletoCharSequenceassertions #2731Details
Verifies that the actual
CharSequenceis 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
isVisibletoCharSequenceassertions #2731Details
Verifies that the actual
CharSequenceis 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
isUnmodifiabletoMapassertions #2458Details
Verifies that the map under test is unmodifiable, i.e. throws an
UnsupportedOperationExceptionwith any attempt to modify the map.Example:
// assertions succeeds assertThat(Collections.unmodifiableMap(new HashMap<>())).isUnmodifiable(); // assertions fails assertThat(new HashMap<>()).isUnmodifiable();
-
Add
isExecutabletoFileassertions #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)toThrowableassertions #2853Details
Verifies that the message of the
Throwableunder test matches the givenPatternregular 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
returnsanddoesNotReturn#2725 - Fix
ignoringOverriddenEqualsForFieldsMatchingRegexesthat was checking field class instead of the field path - Add support for soft assertions when calling
isThrownBy#2699 - Fix
isCloseToJavadoc example forDateassertions #2890
โก Improvements
Core
- Generalize custom format creation to support any
CharSequence#2735 - Improve
containsExactlyerror message by printing the indices where a mismatch occurred #2638 - Throw
org.opentest4j.AssertionFailedErrorforcontainsExactlyto get theactualvs.expecteddiff 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
CaseInsensitiveStringComparatordelegate tocompareToIgnoreCase#2663 - Use actual
Maptype when invoking copying constructor #2710 - Improve
isBetweenassertion performance by only building the error message when needed #2884 - Avoid a copy in
assertContainswhenactualis already aCollection#2220 - Repair code style issues in
org.assertj.core.internal.Dates#2763 - Reduce
RealNumbersconstructors' visibility toprotected#2772 - Use
getParameterCountrather thangetParameterTypes().length#2813 - Return cache in
setup-javaaction #2802 - Fix test results summary #2803
- Remove
publicmodifier to fix some sonar violations - Clean up unused imports
- Migrate
MatcherShouldMatch_create_TestandStandardRepresentation_toStringOf_AtomicReferences_Testto 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-labelto 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
Pathtests #2826 - Reuse mocks and spies in
Filetests #2827 - Remove unnecessary test for
MapEntry::hashCode - Improve JaCoCo configuration
- Use
oracle-actions/setup-javafor 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!