-
-
Save olithissen/0855d0c21880f98f0df69e67a60891ea to your computer and use it in GitHub Desktop.
package net.tonick.demo | |
import org.junit.jupiter.api.function.Executable; | |
import java.util.ArrayList; | |
import java.util.List; | |
import static org.junit.jupiter.api.Assertions.fail; | |
/** | |
* AssertCollector is a utility for collecting and executing assertions in JUnit tests. | |
* It allows for multiple assertions to be executed and collected for later inspection without | |
* repeating your run, fix, run, fix cycle over and over again. | |
* | |
* <p>Example usage</p> | |
* | |
* <pre>{@code | |
* AssertCollector defaultCollector = new AssertCollector("Demo"); | |
* defaultCollector.add( | |
* () -> assertEquals(1, 1, "Strangely 1 is not equal to 1")), | |
* () -> assertTrue(false, "Wait, false is not true?")) | |
* ); | |
* | |
* // ... | |
* | |
* defaultCollector.add(() -> fail("Fail anyway")); | |
* | |
* // ... | |
* | |
* if (defaultCollector.hasTestFailures()) { | |
* fail(defaultCollector.getName() + " has failures: " + String.join("\n", defaultCollector.getMessages())); | |
* } | |
* | |
* | |
* }</pre> | |
* | |
* Alternatively the test code can be wrapped in a try-with-resources statement to automatically fail on `close()` | |
* if there are messages present. | |
* <pre>{@code | |
* try (AssertCollector defaultCollector = new AssertCollector("Demo")) { | |
* // ... | |
* } | |
* | |
* }</pre> | |
*/ | |
public class AssertCollector implements AutoCloseable { | |
private final String name; | |
private List<String> messages = new ArrayList<>(); | |
/** | |
* Create AssertCollector with default name | |
*/ | |
public AssertCollector() { | |
this("Default"); | |
} | |
/** | |
* Create AssertCollector with a specified name | |
* | |
* @param name The AsserCollector's name | |
*/ | |
public AssertCollector(String name) { | |
this.name = name; | |
} | |
/** | |
* Returns the name of the AssertCollector ... duh... | |
* | |
* @return The AsserCollector's name | |
*/ | |
public String getName() { | |
return name; | |
} | |
/** | |
* Returns the list of messages collected during assertion execution. | |
* | |
* @return the list of messages collected during execution | |
*/ | |
public List<String> getMessages() { | |
return messages; | |
} | |
/** | |
* Checks if there are any of the asserts failed | |
* | |
* @return `true` if there are test failures, `false` otherwise | |
*/ | |
public boolean hasTestFailures() { | |
return !messages.isEmpty(); | |
} | |
/** | |
* Adds and executes the given array of `Executable` assertions. | |
* Any failure messages are collected for later inspection. | |
* | |
* @param executables the array of `Executable` assertions to be executed | |
*/ | |
public void add(Executable... executables) { | |
for (var executable : executables) { | |
try { | |
executable.execute(); | |
} catch (Throwable t) { | |
messages.add(t.getMessage()); | |
} | |
} | |
} | |
@Override | |
public void close() throws Exception { | |
if (hasTestFailures()) { | |
fail("'%s' has failures: %s".formatted(getName(), String.join(System.lineSeparator(), getMessages()))); | |
} | |
} | |
} |
As a side note, we provide our own support for soft assertions in Spring Framework's testing support (for testing web applications), and we use an ExceptionCollector that is similar to your AssertCollector
.
One thing to keep in mind is that developers may wish to see all exceptions. JUnit Jupiter's assertAll()
, Spring's ExceptionCollector
, and AssertJ's soft assertion support all achieve that via suppressed exceptions.
I think it would be safer to make AssertCollector
implement ExtensionContext.Store.CloseableResource
and inject it as a parameter from an extension. That way, one cannot forget to "close" it. The extension can be implemented as a separate class, of course.
public class SoftAssertionsTests {
@RegisterExtension
Extension parameterResolver = new ParameterResolver() {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return AssertCollector.class.equals(parameterContext.getParameter().getType());
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return extensionContext.getStore(ExtensionContext.Namespace.create(getClass(), parameterContext.getParameter()))
.getOrComputeIfAbsent(AssertCollector.class, __ -> new AssertCollector(), AssertCollector.class);
}
};
@Test
void test(AssertCollector collector) {
collector.add(() -> fail("1"));
collector.add(() -> fail("2"));
}
}
As Gists don't support emoji reactions, I'm giving all this a collective 👍
I didn't expect this kind of feedback from people as deeply involved in JUnit. I'll have a look into all the suggestions.
TBH, whenever I need something like this, I use soft assertions.
First introduced in TestNG and supported very nicely by AssertJ.
AssertJ provides several different APIs and extensions for working with soft assertions, but I personally prefer the following variant with a static import for
SoftAssertions.assertSoftly
.