Skip to content

Instantly share code, notes, and snippets.

@olithissen
Last active November 12, 2024 08:41
Show Gist options
  • Save olithissen/0855d0c21880f98f0df69e67a60891ea to your computer and use it in GitHub Desktop.
Save olithissen/0855d0c21880f98f0df69e67a60891ea to your computer and use it in GitHub Desktop.
A small Java util class to don't have JUnit 5 fail on each assert. Handy if you have multiple asserts in one test and don't want to fix each failure one by one ...
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())));
}
}
}
@sormuras
Copy link

Either that, or use fail() as you did in the javadoc comment.

@sbrannen
Copy link

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.

  assertSoftly(softly -> {
    softly.assertThat("George Martin").as("great authors").isEqualTo("JRR Tolkien");
    softly.assertThat(42).as("response to Everything").isGreaterThan(100);
    softly.assertThat("Gandalf").isEqualTo("Sauron");
    // no need to call assertAll(), assertSoftly does it for us.
  });

@sbrannen
Copy link

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.

@marcphilipp
Copy link

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

@olithissen
Copy link
Author

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.

@sormuras @sbrannen @marcphilipp

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