Add suppressedExceptions() navigation method to Throwable assertions#4194
Add suppressedExceptions() navigation method to Throwable assertions#4194
suppressedExceptions() navigation method to Throwable assertions#4194Conversation
suppressedExceptions() navigation method to Throwable assertions
3db8861 to
1eba1eb
Compare
This comment has been minimized.
This comment has been minimized.
|
I don't have anything special to say regarding the navigation. However, I have one question regarding:
This makes a lot of sense. However, how does this affect the existing |
From a signature perspective, only the abstract one would be visible to end users (after being renamed to The new For example, today we have: assertj/assertj-core/src/main/java/org/assertj/core/api/Assertions.java Lines 699 to 701 in cacf4ef Tomorrow, public static <T> ObjectAssert<T> assertThat(T actual) { // ObjectAssert is abstract!
return ObjectAssert.for(actual); // this instantiates a concrete subtype of ObjectAssert, never exposed to end users
}Given the massive impact on existing signatures, I foresee that in version 4 only. Does it make sense so far, or do you see any problems with this strategy? |
Currently the How would the It's exposed it via public static AbstractCharSequenceAssert<?, ? extends CharSequence> assertThat(CharSequence actual) {
return AssertionsForInterfaceTypes.assertThat(actual);
}How would the return type look like here? |
|
|
1eba1eb to
492b670
Compare
This comment has been minimized.
This comment has been minimized.
492b670 to
52564e7
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
This PR adds a new navigation assertion for Throwable suppressed exceptions, enabling fluent array assertions on Throwable#getSuppressed() and the ability to navigate back to the originating throwable assertion. It also refactors several tests to reuse a shared ThrowingCallableFactory helper.
Changes:
- Add
suppressedExceptions()navigation method toAbstractThrowableAssertandwithSuppressedExceptionsThat()toThrowableAssertAlternative. - Introduce
SuppressedExceptionsAssertto support array assertions on suppressed exceptions andreturnToThrowable()navigation. - Add/adjust integration tests around suppressed exceptions navigation and consolidate
codeThrowing(...)helper usage.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| assertj-core/src/main/java/org/assertj/core/api/AbstractThrowableAssert.java | Adds suppressedExceptions() navigation method returning SuppressedExceptionsAssert. |
| assertj-core/src/main/java/org/assertj/core/api/ThrowableAssertAlternative.java | Adds withSuppressedExceptionsThat() delegating to the core throwable assertion navigation. |
| assertj-core/src/main/java/org/assertj/core/api/SuppressedExceptionsAssert.java | New assertion type for suppressed exceptions with returnToThrowable(). |
| assertj-tests/assertj-integration-tests/assertj-core-tests/src/test/java/org/assertj/tests/core/api/throwable/ThrowableAssert_suppressedExceptions_Test.java | Tests for the new ThrowableAssert#suppressedExceptions() navigation method and state propagation. |
| assertj-tests/assertj-integration-tests/assertj-core-tests/src/test/java/org/assertj/tests/core/api/ThrowableAssertAlternative_withSuppressedExceptionsThat_Test.java | Tests for ThrowableAssertAlternative#withSuppressedExceptionsThat(). |
| assertj-tests/assertj-integration-tests/assertj-core-tests/src/test/java/org/assertj/tests/core/api/suppressedexceptions/SuppressedExceptionsAssert_Test.java | Tests basic structure/behavior of SuppressedExceptionsAssert including navigation back to origin. |
| assertj-tests/assertj-integration-tests/assertj-core-tests/src/test/java/org/assertj/tests/core/api/SoftAssertions_ThrowableTypeAssert_Test.java | Fixes package/imports and reuses shared codeThrowing(...) helper. |
| assertj-tests/assertj-integration-tests/assertj-core-tests/src/test/java/org/assertj/tests/core/api/BDDSoftAssertions_ThrowableTypeAssert_Test.java | Fixes package/imports and reuses shared codeThrowing(...) helper. |
| assertj-tests/assertj-integration-tests/assertj-core-tests/src/test/java/org/assertj/tests/core/api/Assertions_catchThrowable_Test.java | Replaces local codeThrowing(...) helper with shared factory. |
| assertj-tests/assertj-integration-tests/assertj-core-tests/src/test/java/org/assertj/tests/core/api/Assertions_catchRuntimeException_Test.java | Updates static import to shared codeThrowing(...) helper. |
| assertj-tests/assertj-integration-tests/assertj-core-tests/src/test/java/org/assertj/tests/core/api/Assertions_assertThatThrownBy_Test.java | Replaces local codeThrowing(...) helper with shared factory. |
| assertj-core/src/test/java/org/assertj/core/testkit/ThrowingCallableFactory.java | Removes duplicate helper in favor of the shared testkit version under assertj-tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
52564e7 to
15558cb
Compare
This comment has been minimized.
This comment has been minimized.
15558cb to
842181e
Compare
This comment has been minimized.
This comment has been minimized.
842181e to
48c9b47
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
48c9b47 to
479c8f0
Compare
✅ All tests passed ✅🏷️ Commit: 479c8f0 Learn more about TestLens at testlens.app. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public abstract class SuppressedExceptionsAssert<ORIGIN extends AbstractThrowableAssert<ORIGIN, THROWABLE>, THROWABLE extends Throwable> | ||
| extends AbstractObjectArrayAssert<SuppressedExceptionsAssert<ORIGIN, THROWABLE>, Throwable> { | ||
|
|
||
| private final ORIGIN originAssert; | ||
|
|
||
| static <ORIGIN extends AbstractThrowableAssert<ORIGIN, THROWABLE>, THROWABLE extends Throwable> SuppressedExceptionsAssert<ORIGIN, THROWABLE> from(ORIGIN originAssert) { | ||
| return new DefaultAssert<>(originAssert, originAssert.actual.getSuppressed()).withAssertionState(originAssert); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new instance from an {@link ORIGIN} assert instance and an array of suppressed exceptions. | ||
| * | ||
| * @param originAssert the {@link ORIGIN} assert that initiated the navigation. | ||
| * @param suppressedExceptions the suppressed exceptions. | ||
| */ | ||
| protected SuppressedExceptionsAssert(ORIGIN originAssert, Throwable[] suppressedExceptions) { | ||
| super(suppressedExceptions, SuppressedExceptionsAssert.class); | ||
| this.originAssert = requireNonNull(originAssert, shouldNotBeNull("originAssert")::create); |
There was a problem hiding this comment.
SuppressedExceptionsAssert is described (in the PR description) as the public extension point, but its type signature hard-codes the self type as SuppressedExceptionsAssert<...> (it extends AbstractObjectArrayAssert<SuppressedExceptionsAssert<...>, ...> and passes SuppressedExceptionsAssert.class to super). This prevents subclasses from getting fluent return types for inherited assertion methods. If third-party extension is a goal, consider adding a SELF extends SuppressedExceptionsAssert<SELF, ORIGIN, THROWABLE> type parameter (and forwarding selfType into the constructor) to preserve fluent chaining for subclasses.
| Class<?> proxyClass = createSoftAssertionProxyClass(SuppressedExceptionsAssert.class); | ||
| try { | ||
| AbstractThrowableAssert<?, ?> originAssert = suppressedExceptionsAssert.returnToThrowable(); | ||
| Constructor<?> constructor = proxyClass.getConstructor(AbstractThrowableAssert.class, Throwable[].class); |
There was a problem hiding this comment.
createSuppressedExceptionsAssertProxy looks up the proxy constructor with getConstructor(...), which only finds public constructors. SuppressedExceptionsAssert's constructor is protected (and the proxy likely mirrors that), so this can throw NoSuchMethodException at runtime when soft assertions navigate to suppressedExceptions(). Use getDeclaredConstructor(...) (and make it accessible) or otherwise ensure the generated proxy exposes a public constructor for this signature.
| Constructor<?> constructor = proxyClass.getConstructor(AbstractThrowableAssert.class, Throwable[].class); | |
| Constructor<?> constructor = proxyClass.getDeclaredConstructor(AbstractThrowableAssert.class, Throwable[].class); | |
| constructor.setAccessible(true); |
Originally inspired by 013b35a.
suppressedExceptions()navigation method toThrowableassertions #3858SuppressedExceptionsAssertto3.x#4183This change also demonstrates the new architecture I'd like to target for all assertion classes, which generally provides more user-friendly class names, a smaller public API surface, and less boilerplate.
Pros
Simpler class names in the public API
The class name exposed to end users no longer includes the "Abstract" prefix (in this case,
SuppressedExceptionsAssert), which improves the readability of metadata displayed in editors such as IntelliJ IDEA.For example, the changes in this PR are shown as:
while the changes from 013b35a are shown as:
Smaller Public Surface and Controlled Instantiation
Only the abstract contract is public, and it is the sole extension point third-party authors should consider. On the other side, the concrete implementation is private (in this case,
SuppressedExceptionsAssert.Default), which means less surface to keep stable and more freedom for future refactoring.In addition, a single factory method (in this case,
SuppressedExceptionsAssert#from(...)) centralizes the instantiation logic and ensures correct state propagation (e.g.,.withAssertionState(sourceAssert)). While this factory method is currently package-private, we might consider promoting it to public if a concrete use case arises.Cons
assertj-generatormight have increased complexity when dealing with this pattern. However, theassertThatpublic entry points should always be preferred for obtaining assertion instances.