Skip to content

Prevent extracting from leaking RuntimeException in soft assertions#4219

Open
filiphr wants to merge 1 commit intoassertj:mainfrom
filiphr:skip-softassertions-extracting-null
Open

Prevent extracting from leaking RuntimeException in soft assertions#4219
filiphr wants to merge 1 commit intoassertj:mainfrom
filiphr:skip-softassertions-extracting-null

Conversation

@filiphr
Copy link
Copy Markdown
Contributor

@filiphr filiphr commented Apr 12, 2026

This is a spike on skipping errors in chained assertions. Similar to PR #4218 and should fix #4213.


When actual is null, extracting internally fails its null-guard with an AssertionError (collected by soft assertions), then continues to apply the extractor to null and leaks a RuntimeException that bypasses soft collection.

Introduce a skipAssertions dead-chain flag on AbstractAssert, propagated via withAssertionState. When a soft null-guard in extracting fails, return a dead-chain navigated assertion so downstream assertions no-op instead of throwing or piling up errors that cannot succeed.

Check List:

When `actual` is null, `extracting` internally fails its null-guard with
an AssertionError (collected by soft assertions), then continues to apply
the extractor to null and leaks a RuntimeException that bypasses soft
collection.

Introduce a `skipAssertions` dead-chain flag on AbstractAssert, propagated
via `withAssertionState`. When a soft null-guard in `extracting` fails,
return a dead-chain navigated assertion so downstream assertions no-op
instead of throwing or piling up errors that cannot succeed.
@testlens-app
Copy link
Copy Markdown

testlens-app bot commented Apr 12, 2026

✅ All tests passed ✅

🏷️ Commit: d294223
▶️ Tests: 20735 executed
⚪️ Checks: 18/18 completed


Learn more about TestLens at testlens.app.

}

private <T, ASSERT extends AbstractAssert<?, ?>> ASSERT deadChainNavigation(AssertFactory<T, ASSERT> assertFactory) {
return markAsDeadChain(assertFactory.createAssert((T) null));
Copy link
Copy Markdown
Member

@joel-costigliola joel-costigliola Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be a bit simpler, since we can't really extract anything, we could simply return this assertion after setting the skipAssertions to true, I mean at this point since we failed the navigation, it would make sense not to navigate at all.

I suggest

  • removing markAsDeadChain,
  • changing visibility of this method to protected so that users can override it,
  • implementing it like:
skipChainedAssertions = true;
// Noop assert since we just have skipChainedAssertions
return (ASSERT)myself;

For navigation that returns a different type of assertion, we can - as you did for the list assertion - return an assertion with actual set to null.

WDYT ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure that I am following.

In this particular case we can't do what you are suggestion since this method is for navigation, i.e. we can't just do `(ASSERT)myself) it'll lead to a class cast exception.

This method is used in the 2 extracting from AbstractAssert that take an AssertFactory and return a different type of assertion.

Also in the AbstractObjectAssert, I can't return myself as it expects a AbstractListAssert as a return type.

Perhaps we can only keep markAsDeadChain and do

if (actual == null) return markAsDeadChain(assertFactory.createAssert((T) null));

If you like the approach I can go ahead and implement it for the rest of the classes to see how it would look like fully.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since extracting a single value returns AbstractAssert, you can return any abstract assert including myself as long as you set the skip assertions flag to true, since it's a "dead" assert instance it does not matter to build a precise subtype of assert (I'm probably not much clearer than my previous comment but I don't know how to express my point in a clearer way).

I have tried with my suggested implementation on your branch branch locally, ie this:

  protected <ASSERT extends AbstractAssert<?, ?>> ASSERT extracting(String propertyOrField,
                                                                    AssertFactory<Object, ASSERT> assertFactory) {
    requireNonNull(propertyOrField, shouldNotBeNull("propertyOrField")::create);
    requireNonNull(assertFactory, shouldNotBeNull("assertFactory")::create);
    isNotNull();
    if (actual == null) {
      skipAssertions = true;
      return (ASSERT)myself;
    }
   ....  

and all the SoftAssertions_extracting_on_null_actual_Test are green.

I also tested with strongly typed extracting and it works too, ex:

  @Test
  void should_not_throw_when_extracting_by_name_without_explicit_isNotNull() {
    // GIVEN / WHEN
    softly.assertThat((Object) null).extracting("foo", STRING).startsWith("bar"); // extract as String
    // THEN - extracting's internal isNotNull collects one error; downstream is noop
    List<Throwable> errors = softly.errorsCollected();
    then(errors).hasSize(1);
    then(errors.get(0)).hasMessageContaining("Expecting actual not to be null");
  }

@filiphr I'm more than happy to be proven wrong, could you think of a test that would break my suggested implementation ?

we can't do that for extracting multiple values since it returns a list assert, in this case we obviously can't return myself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've checked this a bit more. Yes you are right about the AbstractAssert#extracting. However, that pattern won't work for the AbsractIterable#first(InstanceOfAssertFactory).

I'll try to find some time to apply a general pattern for everything

Copy link
Copy Markdown
Member

@joel-costigliola joel-costigliola Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it won't work with first() since myself is an iterable assert and first and object assert.
In that case, simply returning an ObjectAssert would work.

BTW I find the deadChain term to be quite good and expressive
Keen to see where your exploration is going to go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Soft assertions fail in confusing ways with null values

2 participants