Skip to content

v3.24.0

Compare
Choose a tag to compare
@scordio scordio released this 12 Sep 17:18
· 465 commits to main since this release

🚫 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 and hasNoNullFields recursive assertions #2211

    Details

    allFieldsSatisfy verifies that a given Predicate 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, then allFieldsSatisfy 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 test
    • ignoringFieldsMatchingRegexes(String... regexes) - the assertion ignores the fields matching the specified regexes in the object under test
    • ignoringFieldsOfTypes(Class<?>... typesToIgnore) - the assertion ignores the object under test fields of the given types
    • ignoringPrimitiveFields() - 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 #2711

    Details

    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 a Bar field and both have an id field, one can register a BiPredicate for Foo and Bar 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 #2794

    Details

    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 comparing Person.name, Person.address and all other Person fields. In case actual's field is null, expected's field type will be checked to match one of the given types (we assume actual and expected fields have the same type).

    comparingOnlyFieldsOfTypes can be combined with comparingOnlyFields(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 resulting compared 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 fields String foo, Integer baz and Double bar and the ignored fields = {"bar"} set with ignoringFields(String...) that will remove bar 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 #2794

    Details

    isIn verifies that actual is present in the given iterable/array, while isNotIn verifies that actual 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 #2691

    Details

    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 to Class assertions #2611

    Details

    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 ease Future / CompletableFuture throwable assertions #2704

    Details

    Returns a ThrowableAssertAlternative to chain assertions on the underlying throwable for Future / 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 #2723

    Details

    Verifies that the actual CharSequence matches the given regular expression pattern, then accepts the given Consumer<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 #2726

    Details

    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 to CharSequence assertions #2731

    Details

    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 to CharSequence assertions #2731

    Details

    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 to CharSequence assertions #2731

    Details

    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 to CharSequence assertions #2731

    Details

    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 to CharSequence assertions #2731

    Details

    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 to CharSequence assertions #2731

    Details

    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 to Map assertions #2458

    Details

    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 to File assertions #2829

    Details

    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) to Throwable assertions #2853

    Details

    Verifies that the message of the Throwable under test matches the given Pattern 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 and doesNotReturn #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 for Date 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 for containsExactly to get the actual 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 to compareToIgnoreCase #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 when actual is already a Collection #2220
  • Repair code style issues in org.assertj.core.internal.Dates #2763
  • Reduce RealNumbers constructors' visibility to protected #2772
  • Use getParameterCount rather than getParameterTypes().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 and StandardRepresentation_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 in binary-compatibility.yml #2667
  • Improve CI output #2669
  • Reset global state more aggressively in tests #2670
  • Fix incomplete test for given(Duration) in BDDAssumptionsTest #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!