Skip to content

Commit

Permalink
Introduce ExceptionCollector testing utility
Browse files Browse the repository at this point in the history
This commit introduces a new ExceptionCollector testing utility in order
to support "soft assertion" use cases.

Closes gh-27316
  • Loading branch information
sbrannen committed Aug 23, 2021
1 parent 8a7c4fc commit 81a6ba4
Show file tree
Hide file tree
Showing 2 changed files with 278 additions and 0 deletions.
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;

}

}
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");
};
}

}

0 comments on commit 81a6ba4

Please sign in to comment.