Assertj-core: SoftAssertions nested in satisfies() or allSatisfy() only output first failing assertion

Created on 2 Nov 2018  路  9Comments  路  Source: assertj/assertj-core

Summary

When using SoftAssertions together with satisfies() or allSatisfy() the additional assertions made in the consumer supplied to these methods are handled differently. In fact, the first failing assertion prevents other in the same consumer to be reported, as expected.

Example

public class MyTest {

  @Test
  public void softAssertionTest() {
    String str = "hello";

    SoftAssertions.assertSoftly(sa -> {
      sa.assertThat(str).satisfies(s -> {

        // fails
        sa.assertThat(s).hasSize(2);

        // not reported, unless all previous assertions succeed
        sa.assertThat(s).isEmpty();
      });
    });
  }
}
bug

Most helpful comment

You beat me by 2 min! :D

All 9 comments

This should work, thanks for reporting this.

As far as I understood the code, it is a problem regarding the SoftProxies and nesting with respect to the ErrorCollector. The nested satisfy block seems to let errors propagate, which results in the first assertion (failing) to prevent follow up assertions to even execute.

It seems the problems comes from the way AssertJ handles nested proxy calls.

Example:
softly.assertThat(true).isFalse() calls the isEqualTo(false) which is also proxied, in that case the assertion error is bubbled up. It looks like in this issue the assertion error should simply be recorded and not bubbled up .

We count the nested level by count of ErrorCollector.intercept which happens with satisfies and the assertions inside (hasSize).

org.eclipse.jdt.internal.junit.runner.RemoteTestRunner at localhost:36031   
    Thread [main] (Suspended)   
        ErrorCollector.intercept(ErrorCollector, Object, Callable<?>, Method, Object) line: 64  
        StringAssert$ByteBuddy$ZwZnwlBB.hasSize(int) line: not available    
        StringAssert$ByteBuddy$ZwZnwlBB.hasSize(int) line: not available    
        SoftAssertionsErrorDescriptionTest.lambda$2(SoftAssertions, String) line: 51    
        2052223881.accept(Object) line: not available   
        StringAssert$ByteBuddy$ZwZnwlBB(AbstractAssert<SELF,ACTUAL>).satisfies(Consumer<ACTUAL>) line: 669  
        StringAssert$ByteBuddy$ZwZnwlBB.satisfies$accessor$Qnd4MFMu(Consumer) line: not available   
        StringAssert$ByteBuddy$ZwZnwlBB$AssertJ$SoftProxies$Im2qiR2u.call() line: not available 
        ErrorCollector.intercept(ErrorCollector, Object, Callable<?>, Method, Object) line: 58  
        StringAssert$ByteBuddy$ZwZnwlBB.satisfies(Consumer<String>) line: not available 
        StringAssert$ByteBuddy$ZwZnwlBB.satisfies(Consumer) line: not available 
        SoftAssertionsErrorDescriptionTest.lambda$1(String, SoftAssertions) line: 54    
        1836463382.accept(Object) line: not available   
        SoftAssertions.assertSoftly(Consumer<SoftAssertions>) line: 161 
        SoftAssertionsErrorDescriptionTest.softAssertionTest() line: 47 
        NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]  
        NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62  
        DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43  
        Method.invoke(Object, Object...) line: 498  
        ReflectionUtils.invokeMethod(Method, Object, Object...) line: 515   
        ExecutableInvoker.invoke(Method, Object, ExtensionContext, ExtensionRegistry) line: 115 
        TestMethodTestDescriptor.lambda$invokeTestMethod$6(ExtensionContext, JupiterEngineExecutionContext) line: 171   
        106374177.execute() line: not available 
        OpenTest4JAwareThrowableCollector(ThrowableCollector).execute(ThrowableCollector$Executable) line: 72   
        TestMethodTestDescriptor.invokeTestMethod(JupiterEngineExecutionContext, Node$DynamicTestExecutor) line: 167    
        TestMethodTestDescriptor.execute(JupiterEngineExecutionContext, Node$DynamicTestExecutor) line: 114 
        TestMethodTestDescriptor.execute(EngineExecutionContext, Node$DynamicTestExecutor) line: 59 
        NodeTestTask<C>.lambda$executeRecursively$5() line: 105 
        957465255.execute() line: not available 
        OpenTest4JAwareThrowableCollector(ThrowableCollector).execute(ThrowableCollector$Executable) line: 72   
        NodeTestTask<C>.executeRecursively() line: 95   
        NodeTestTask<C>.execute() line: 71  
        367746789.accept(Object) line: not available    
        ArrayList<E>.forEach(Consumer<? super E>) line: 1257    
        SameThreadHierarchicalTestExecutorService.invokeAll(List<TestTask>) line: 38    
        NodeTestTask<C>.lambda$executeRecursively$5() line: 110 
        957465255.execute() line: not available 
        OpenTest4JAwareThrowableCollector(ThrowableCollector).execute(ThrowableCollector$Executable) line: 72   
        NodeTestTask<C>.executeRecursively() line: 95   
        NodeTestTask<C>.execute() line: 71  
        367746789.accept(Object) line: not available    
        ArrayList<E>.forEach(Consumer<? super E>) line: 1257    
        SameThreadHierarchicalTestExecutorService.invokeAll(List<TestTask>) line: 38    
        NodeTestTask<C>.lambda$executeRecursively$5() line: 110 
        957465255.execute() line: not available 
        OpenTest4JAwareThrowableCollector(ThrowableCollector).execute(ThrowableCollector$Executable) line: 72   
        NodeTestTask<C>.executeRecursively() line: 95   
        NodeTestTask<C>.execute() line: 71  
        SameThreadHierarchicalTestExecutorService.submit(TestTask) line: 32 
        HierarchicalTestExecutor<C>.execute() line: 57  
        JupiterTestEngine(HierarchicalTestEngine<C>).execute(ExecutionRequest) line: 51 
        DefaultLauncher.execute(TestEngine, ExecutionRequest) line: 170 
        DefaultLauncher.execute(Root, ConfigurationParameters, TestExecutionListener...) line: 154  
        DefaultLauncher.execute(LauncherDiscoveryRequest, TestExecutionListener...) line: 90    
        JUnit5TestReference.run(TestExecution) line: 89 
        TestExecution.run(ITestReference[]) line: 41    
        RemoteTestRunner.runTests(String[], String, TestExecution) line: 541    
        RemoteTestRunner.runTests(TestExecution) line: 763  
        RemoteTestRunner.run() line: 463    
        RemoteTestRunner.main(String[]) line: 209   
    Thread [ReaderThread] (Running) 

You beat me by 2 min! :D

One workaround is simply not to use sa to call satisfies:

 SoftAssertions.assertSoftly(sa -> {
      // don't call with like sa.assertThat.satisfies( ... 
      assertThat(str).satisfies(s -> {

        // fails
        sa.assertThat(s).hasSize(2);

        // not reported, unless all previous assertions succeed
        sa.assertThat(s).isEmpty();
      });
    });

I know that this is a workaround, but the example given was greatly simplified. This would also affect not using allSatisfy(), I assume, which i do use very often.

I don't see an easy fix for this issue, I'll try to explain why.

Some of the assertions internally use others like isNotNull() as in:

  public SELF contains(VALUE expectedValue) {
    isNotNull(); // check actual is not null
    if (!actual.isPresent()) throwAssertionError(shouldContain(expectedValue));
    ...

Soft assertion are implemented by proxying assertions and catching any AssertionErrror and report all of them later on.
In the example above, contains and isNotNull() are proxied, if isNotNull() fails the thrown AssertionErrror will be collected and the next line of code executed which will result in a NPE, not what we want!
So what AssertJ does is to propagate the error to contains and let the proxy mechanism to collect it (there is one collector per soft assertion instance). That's better.
Oh and to do that, it counts the number of calls to ErrorCollector.intercept from the stack trace, if > 1 it will see an internal call and propagate the error (in ErrorCollector).

Now in your example, you are nesting soft assertions so the the number of calls to ErrorCollector.intercept will be > 1 which leads AssertJ to think that it is in a situation like contains calling isNotNull(), it propagates the AssertionError and this is why the next assertion is not performed.

To fix that, we would need to know that the nesting of proxied assertions was intended by the user and not an internal implementation, I'm not sure this is quite possible but I need to give it more time, there might be a solution that I don't see at the moment.

Isn't the "nesting" limited to calling satisfies() or allSatisfy(), so we could handle those two differently? Let alone the fact, that we could have deeper nesting as well, for instances satisfy() within satisfy().

It's not that we have a major issue with the current behavior. As long as all assertions succeed, we are sure the test cases are covered. Mainly, the issue comes up during refactorings, if many assertions should fail at once, which is why we use SoftAssertions in the first place.

Maybe but it should be handled only if coming from a soft assertion ... need trying/investigating.

I was also thinking that it might be possible to differentiate internal AssertJ calls from users ones by looking the stack trace to see if the caller of intercept was made from org.assertj or not.

This issue is a minor bugfix since AssertJ does not lead to false positive, as you said if the assertions succeed the test case is covered, and the test would fail if any assertion fails.

Was this page helpful?
0 / 5 - 0 ratings