Core Java

JUnit Print Assertion Results Example

JUnit assertions are designed to remain silent on success and report output only when a failure occurs. This behaviour helps keep test execution output concise, but it can limit visibility when deeper insight into assertion values or failure conditions is required.

Although JUnit does not provide a native mechanism for printing assertion results, it offers several extensible features such as custom assertion messages, logging, and lifecycle callbacks that can be used to surface meaningful diagnostic information in a structured manner.

This article explains how JUnit assertions work and presents strategies for making assertion outcomes more visible in JUnit 5.

1. Understanding JUnit Assertions

JUnit assertions validate expected behaviour by comparing expected and actual values. If an assertion passes, execution continues silently. If it fails, JUnit throws an AssertionError, causing the test to fail.

Example:

assertEquals(10, result);

When an assertion passes, no output is produced, but when it fails, test execution stops, and an error message is reported. This design helps keep test output clean and focused, but it can make debugging more challenging when additional context is needed.

Example Application Class

All examples in this article use the following simple service class.

public class Calculator {

    public int add(int a, int b) {
        return a + b;
    }

    public int divide(int a, int b) {
        return a / b;
    }
}

2. Logging Assertion Context Before Execution

A common approach is to log the assertion context before performing the assertion. This ensures the values are visible whether the assertion passes or fails.

class CalculatorTest {

    private static final Logger logger = Logger.getLogger(CalculatorTest.class.getName());

    private final Calculator calculator = new Calculator();

    @Test
    void shouldAddTwoNumbers() {
        int a = 4;
        int b = 6;

        int result = calculator.add(a, b);

        logger.info(() -> String.format("Asserting addition result: %d + %d = %d", a, b, result));

        assertEquals(10, result, "Addition result is incorrect");
    }
}

Output (Test Pass)

Feb 08, 2026 5:54:30 A.M. com.jcg.example.CalculatorTest shouldAddTwoNumbers
INFO: Asserting addition result: 4 + 6 = 10

This approach provides runtime visibility without polluting the assertion itself.

Logging with Descriptive Assertion Messages

Assertion messages are shown only when a test fails, which makes them useful for clearly explaining what went wrong.

public class CalculatorFailureTest {

    private static final Logger logger = Logger.getLogger(CalculatorFailureTest.class.getName());

    private final Calculator calculator = new Calculator();

    @Test
    void shouldFailWhenSumIsIncorrect() {
        int result = calculator.add(2, 3);

        logger.info(() -> "Computed sum: " + result);

        assertEquals(10, result, "Expected sum to be 10 but was " + result);
    }
}

Output (Test Failure)

INFO: Computed sum: 5
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.021 s <<< FAILURE! -- in com.jcg.example.CalculatorFailureTest
[ERROR] com.jcg.example.CalculatorFailureTest.shouldFailWhenSumIsIncorrect -- Time elapsed: 0.693 s <<< FAILURE!
org.opentest4j.AssertionFailedError: Expected sum to be 10 but was 5 ==> expected: <10> but was: <5>

3. Catching and Logging Assertion Failures

In some scenarios, we may want to intercept assertion failures, log additional context, and still allow the test to fail normally. This is useful when debugging complex test flows or when we want consistent, enriched logging for every failed assertion without duplicating code across test classes.

By centralizing assertions in a helper class, we can catch AssertionError, log detailed diagnostic information, and then rethrow the exception so that JUnit correctly marks the test as failed. This approach improves observability while preserving standard JUnit behaviour.

Custom Assertion Helper with Exception Handling

public final class LoggedAssertions {

    private static final Logger logger = Logger.getLogger(LoggedAssertions.class.getName());

    private LoggedAssertions() {
    }

    public static void assertEqualsWithLogging(int expected, int actual) {
        logger.info(() -> String.format("Executing assertion with expected=%d, actual=%d", expected, actual));

        try {
            assertEquals(expected, actual);
        } catch (AssertionError error) {
            logger.severe(() -> String.format("Assertion failed: expected=%d, actual=%d", expected, actual));
            logger.log(Level.SEVERE, "Failure message: {0}", error.getMessage());
            throw error; // rethrow to ensure test still fails
        }
    }
}

This helper logs the intent of the assertion before it is executed, captures detailed diagnostic information when the assertion fails, and records the original assertion error message for clarity. By rethrowing the exception, it ensures that JUnit still reports the failure correctly and that the test result remains accurate.

Test Class Using the Centralized Helper

public class CentralizedAssertionExceptionTest {

    private final Calculator calculator = new Calculator();

    @Test
    void shouldLogAndRethrowAssertionFailure() {
        int result = calculator.add(4, 4);
        LoggedAssertions.assertEqualsWithLogging(20, result);
    }
}

Output (Test Failure)

INFO: Executing assertion with expected=20, actual=8
Feb 10, 2026 8:29:31 A.M. com.jcg.example.LoggedAssertions assertEqualsWithLogging
SEVERE: Assertion failed: expected=20, actual=8
Feb 10, 2026 8:29:31 A.M. com.jcg.example.LoggedAssertions assertEqualsWithLogging
SEVERE: Failure message: expected: <20> but was: <8>
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.024 s <<< FAILURE! -- in com.jcg.example.CentralizedAssertionExceptionTest
[ERROR] com.jcg.example.CentralizedAssertionExceptionTest.shouldLogAndRethrowAssertionFailure -- Time elapsed: 0.681 s <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <20> but was: <8>

This approach is useful when we need consistent assertion logging across a large test suite, especially when debugging multi-step or data-driven tests. It is also valuable when test failures require additional runtime context and when we want richer, more informative logs without having to modify every individual test method.

4. Printing Assertion Failures with TestWatcher (JUnit 5)

JUnit 5 extensions allow us to hook into the test lifecycle and react to events such as test startup, success, or failure. While TestWatcher is commonly used to observe test outcomes, combining it with BeforeEachCallback provides additional context by allowing us to log information before each test is executed.

This combination is useful for tracing test execution flow and correlating setup steps with assertion failures. By implementing both interfaces in a single extension, we can log when a test starts, capture detailed assertion failure information when it fails, and do so in a centralized and reusable manner without modifying individual test methods.

public class AssertionFailureWatcher implements TestWatcher, BeforeEachCallback {

    private static final Logger logger = Logger.getLogger(AssertionFailureWatcher.class.getName());

    @Override
    public void beforeEach(ExtensionContext context) {
        logger.log(Level.INFO, "Starting test: {0}", context.getDisplayName());
    }

    @Override
    public void testFailed(ExtensionContext context, Throwable cause) {
        logger.log(Level.SEVERE, "Test failed: {0}", context.getDisplayName());
        logger.log(Level.SEVERE, "Failure reason: {0}", cause.getMessage());
    }
}

This extension performs two key roles. First, the beforeEach method runs before every test method and logs the name of the test that is about to execute. This helps establish a clear boundary in the logs, especially when multiple tests are executed in sequence. Second, the testFailed method is triggered only when a test fails, allowing us to log assertion failure details such as the failure message and expected versus actual values.

Test Class Using the Extension

@ExtendWith(AssertionFailureWatcher.class)
class TestWatcherExample {

    private final Calculator calculator = new Calculator();

    @Test
    void shouldFailAndBeLoggedByWatcher() {
        int result = calculator.add(2, 3);
        assertEquals(10, result, "Incorrect addition result");
    }
}

In this test class, the extension is registered using @ExtendWith, which automatically applies it to all test methods in the class. No additional logging or error handling logic is required inside the test itself. The test remains focused on behaviour verification, while the extension handles logging concerns transparently.

Output

INFO: Starting test: shouldFailAndBeLoggedByWatcher()
Feb 10, 2026 9:04:37 A.M. com.jcg.example.AssertionFailureWatcher testFailed
SEVERE: Test failed: shouldFailAndBeLoggedByWatcher()
Feb 10, 2026 9:04:37 A.M. com.jcg.example.AssertionFailureWatcher testFailed
SEVERE: Failure reason: Incorrect addition result ==> expected: <10> but was: <5>
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.615 s <<< FAILURE! -- in com.jcg.example.TestWatcherExample
[ERROR] com.jcg.example.TestWatcherExample.shouldFailAndBeLoggedByWatcher -- Time elapsed: 0.468 s <<< FAILURE!
org.opentest4j.AssertionFailedError: Incorrect addition result ==> expected: <10> but was: <5>

When the assertion fails, JUnit triggers the testFailed method of the TestWatcher. The extension then logs the test name, followed by the assertion failure message. JUnit automatically appends the expected and actual values, providing full diagnostic information without requiring any additional code in the test method. This layered logging approach makes it easy to trace test execution from start to failure, improving debuggability while keeping test code clean and focused.

5. Conclusion

In this article, we examined how JUnit handles assertions and why assertion results are not printed by default, then explored practical ways to improve visibility using centralized assertion helpers and JUnit 5 lifecycle extensions, such as TestWatcher and BeforeEachCallback. These approaches make test failures easier to understand, improve debugging efficiency, and provide clearer insight into test execution while keeping test code clean and preserving JUnit’s core behaviour.

6. Download the Source Code

This article examined several ways to print and log JUnit assertion results.

Download
You can download the full source code of this example here: junit print assertion results

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button