-
Notifications
You must be signed in to change notification settings - Fork 37.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce ExceptionCollector testing utility
This commit introduces a new ExceptionCollector testing utility in order to support "soft assertion" use cases. Closes gh-27316
- Loading branch information
Showing
2 changed files
with
278 additions
and
0 deletions.
There are no files selected for viewing
127 changes: 127 additions & 0 deletions
127
spring-test/src/main/java/org/springframework/test/util/ExceptionCollector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/* | ||
* Copyright 2002-2021 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.springframework.test.util; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
|
||
/** | ||
* {@code ExceptionCollector} is a test utility for executing code blocks, | ||
* collecting exceptions, and generating a single {@link AssertionError} | ||
* containing any exceptions encountered as {@linkplain Throwable#getSuppressed() | ||
* suppressed exceptions}. | ||
* | ||
* <p>This utility is intended to support <em>soft assertion</em> use cases | ||
* similar to the {@code SoftAssertions} support in AssertJ and the | ||
* {@code assertAll()} support in JUnit Jupiter. | ||
* | ||
* @author Sam Brannen | ||
* @since 5.3.10 | ||
*/ | ||
public class ExceptionCollector { | ||
|
||
private final List<Throwable> exceptions = new ArrayList<>(); | ||
|
||
|
||
/** | ||
* Execute the supplied {@link Executable} and track any exception thrown. | ||
* @param executable the {@code Executable} to execute | ||
* @see #getExceptions() | ||
* @see #assertEmpty() | ||
*/ | ||
public void execute(Executable executable) { | ||
try { | ||
executable.execute(); | ||
} | ||
catch (Throwable ex) { | ||
this.exceptions.add(ex); | ||
} | ||
} | ||
|
||
/** | ||
* Get the list of exceptions encountered in {@link #execute(Executable)}. | ||
* @return an unmodifiable copy of the list of exceptions, potentially empty | ||
* @see #assertEmpty() | ||
*/ | ||
public List<Throwable> getExceptions() { | ||
return Collections.unmodifiableList(this.exceptions); | ||
} | ||
|
||
/** | ||
* Assert that this {@code ExceptionCollector} does not contain any | ||
* {@linkplain #getExceptions() exceptions}. | ||
* <p>If this collector is empty, this method is effectively a no-op. | ||
* <p>If this collector contains a single {@link Error} or {@link Exception}, | ||
* this method rethrows the error or exception. | ||
* <p>If this collector contains a single {@link Throwable}, this method throws | ||
* an {@link AssertionError} with the error message of the {@code Throwable} | ||
* and with the {@code Throwable} as the {@linkplain Throwable#getCause() cause}. | ||
* <p>If this collector contains multiple exceptions, this method throws an | ||
* {@code AssertionError} whose message is "Multiple Exceptions (#):" | ||
* followed by a new line with the error message of each exception separated | ||
* by a new line, with {@code #} replaced with the number of exceptions present. | ||
* In addition, each exception will be added to the {@code AssertionError} as | ||
* a {@link Throwable#addSuppressed(Throwable) suppressed exception}. | ||
* @see #execute(Executable) | ||
* @see #getExceptions() | ||
*/ | ||
public void assertEmpty() throws Exception { | ||
if (this.exceptions.isEmpty()) { | ||
return; | ||
} | ||
|
||
if (this.exceptions.size() == 1) { | ||
Throwable exception = this.exceptions.get(0); | ||
if (exception instanceof Error) { | ||
throw (Error) exception; | ||
} | ||
if (exception instanceof Exception) { | ||
throw (Exception) exception; | ||
} | ||
AssertionError assertionError = new AssertionError(exception.getMessage()); | ||
assertionError.initCause(exception); | ||
throw assertionError; | ||
} | ||
|
||
StringBuilder message = new StringBuilder(); | ||
message.append("Multiple Exceptions (").append(this.exceptions.size()).append("):"); | ||
for (Throwable exception : this.exceptions) { | ||
message.append('\n'); | ||
message.append(exception.getMessage()); | ||
} | ||
AssertionError assertionError = new AssertionError(message); | ||
this.exceptions.forEach(assertionError::addSuppressed); | ||
throw assertionError; | ||
} | ||
|
||
|
||
/** | ||
* {@code Executable} is a functional interface that can be used to implement | ||
* any generic block of code that potentially throws a {@link Throwable}. | ||
* | ||
* <p>The {@code Executable} interface is similar to {@link java.lang.Runnable}, | ||
* except that an {@code Executable} can throw any kind of exception. | ||
*/ | ||
@FunctionalInterface | ||
interface Executable { | ||
|
||
void execute() throws Throwable; | ||
|
||
} | ||
|
||
} |
151 changes: 151 additions & 0 deletions
151
spring-test/src/test/java/org/springframework/test/util/ExceptionCollectorTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
/* | ||
* Copyright 2002-2021 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.springframework.test.util; | ||
|
||
import org.junit.jupiter.api.Test; | ||
|
||
import org.springframework.test.util.ExceptionCollector.Executable; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; | ||
import static org.assertj.core.api.Assertions.assertThatNoException; | ||
|
||
/** | ||
* Unit tests for {@link ExceptionCollector}. | ||
* | ||
* @author Sam Brannen | ||
* @since 5.3.10 | ||
*/ | ||
public class ExceptionCollectorTests { | ||
|
||
private static final char EOL = '\n'; | ||
|
||
private final ExceptionCollector collector = new ExceptionCollector(); | ||
|
||
|
||
@Test | ||
void noExceptions() { | ||
this.collector.execute(() -> {}); | ||
|
||
assertThat(this.collector.getExceptions()).isEmpty(); | ||
assertThatNoException().isThrownBy(this.collector::assertEmpty); | ||
} | ||
|
||
@Test | ||
void oneError() { | ||
this.collector.execute(error()); | ||
|
||
assertOneFailure(Error.class, "error"); | ||
} | ||
|
||
@Test | ||
void oneAssertionError() { | ||
this.collector.execute(assertionError()); | ||
|
||
assertOneFailure(AssertionError.class, "assertion"); | ||
} | ||
|
||
@Test | ||
void oneCheckedException() { | ||
this.collector.execute(checkedException()); | ||
|
||
assertOneFailure(Exception.class, "checked"); | ||
} | ||
|
||
@Test | ||
void oneUncheckedException() { | ||
this.collector.execute(uncheckedException()); | ||
|
||
assertOneFailure(RuntimeException.class, "unchecked"); | ||
} | ||
|
||
@Test | ||
void oneThrowable() { | ||
this.collector.execute(throwable()); | ||
|
||
assertThatExceptionOfType(AssertionError.class) | ||
.isThrownBy(this.collector::assertEmpty) | ||
.withMessage("throwable") | ||
.withCauseExactlyInstanceOf(Throwable.class) | ||
.satisfies(error -> assertThat(error.getCause()).hasMessage("throwable")) | ||
.satisfies(error -> assertThat(error).hasNoSuppressedExceptions()); | ||
} | ||
|
||
private void assertOneFailure(Class<? extends Throwable> expectedType, String failureMessage) { | ||
assertThatExceptionOfType(expectedType) | ||
.isThrownBy(this.collector::assertEmpty) | ||
.satisfies(exception -> | ||
assertThat(exception) | ||
.isExactlyInstanceOf(expectedType) | ||
.hasNoSuppressedExceptions() | ||
.hasNoCause() | ||
.hasMessage(failureMessage)); | ||
} | ||
|
||
@Test | ||
void multipleFailures() { | ||
this.collector.execute(assertionError()); | ||
this.collector.execute(checkedException()); | ||
this.collector.execute(uncheckedException()); | ||
this.collector.execute(error()); | ||
this.collector.execute(throwable()); | ||
|
||
assertThatExceptionOfType(AssertionError.class) | ||
.isThrownBy(this.collector::assertEmpty) | ||
.withMessage("Multiple Exceptions (5):" + EOL + // | ||
"assertion" + EOL + // | ||
"checked" + EOL + // | ||
"unchecked" + EOL + // | ||
"error" + EOL + // | ||
"throwable"// | ||
) | ||
.satisfies(exception -> | ||
assertThat(exception.getSuppressed()).extracting(Object::getClass).map(Class::getSimpleName) | ||
.containsExactly("AssertionError", "Exception", "RuntimeException", "Error", "Throwable")); | ||
} | ||
|
||
private Executable throwable() { | ||
return () -> { | ||
throw new Throwable("throwable"); | ||
}; | ||
} | ||
|
||
private Executable error() { | ||
return () -> { | ||
throw new Error("error"); | ||
}; | ||
} | ||
|
||
private Executable assertionError() { | ||
return () -> { | ||
throw new AssertionError("assertion"); | ||
}; | ||
} | ||
|
||
private Executable checkedException() { | ||
return () -> { | ||
throw new Exception("checked"); | ||
}; | ||
} | ||
|
||
private Executable uncheckedException() { | ||
return () -> { | ||
throw new RuntimeException("unchecked"); | ||
}; | ||
} | ||
|
||
} |