or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

assertj-integration.mdcontext-runners.mdindex.mdintegration-testing.mdjson-testing.mdoutput-capture.mdtest-configuration.mdtest-properties.mdweb-test-utilities.md
tile.json

output-capture.mddocs/

Output Capture

Utilities for capturing and verifying System.out and System.err output during test execution.

Package

org.springframework.boot.test.system

Prerequisites

  • JUnit 5 (Jupiter)

Core Imports

import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.test.system.CapturedOutput;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.assertj.core.api.Assertions.assertThat;

OutputCaptureExtension

Full Package: org.springframework.boot.test.system.OutputCaptureExtension

JUnit 5 extension for capturing console output.

/**
 * JUnit 5 extension for capturing System.out and System.err
 * Register with @ExtendWith(OutputCaptureExtension.class) on test classes
 * @since 2.2.0
 */
public class OutputCaptureExtension implements
    BeforeAllCallback,
    AfterAllCallback,
    BeforeEachCallback,
    AfterEachCallback,
    ParameterResolver {

    // Package-private constructor - cannot be instantiated directly
    // Must be registered via @ExtendWith annotation
    // Automatically injects CapturedOutput parameter
    // Captures from start of test method
    // Restores original System.out/err after test
}

CapturedOutput Interface

Full Package: org.springframework.boot.test.system.CapturedOutput

public interface CapturedOutput extends CharSequence {
    String getAll();  // Combined stdout + stderr
    String getOut();  // stdout only
    String getErr();  // stderr only

    // CharSequence methods available:
    // length(), charAt(), subSequence(), toString()
    // Enables: assertThat(output).contains("text")
}

OutputCaptureRule (Deprecated)

Full Package: org.springframework.boot.test.system.OutputCaptureRule

JUnit 4 rule for capturing console output. Deprecated in favor of OutputCaptureExtension for JUnit 5.

/**
 * JUnit 4 Rule for capturing System.out and System.err
 * @since 2.2.0
 * @deprecated since 4.0.0 for removal - use OutputCaptureExtension with JUnit 5 instead
 */
@Deprecated(since = "4.0.0", forRemoval = true)
public class OutputCaptureRule implements TestRule, CapturedOutput {

    /**
     * Apply the rule (from TestRule interface)
     */
    public Statement apply(Statement base, Description description);

    /**
     * Get all captured output (implements CapturedOutput)
     */
    public String getAll();

    /**
     * Get captured stdout only
     */
    public String getOut();

    /**
     * Get captured stderr only
     */
    public String getErr();

    /**
     * Get all captured output as string
     */
    public String toString();

    /**
     * Verify output matches a Hamcrest matcher
     * Verification performed after test method executes
     * @param matcher the Hamcrest matcher
     */
    public void expect(Matcher<? super String> matcher);
}

Migration from JUnit 4 to JUnit 5

Old (JUnit 4 with OutputCaptureRule):

import org.junit.Rule;
import org.junit.Test;
import org.springframework.boot.test.system.OutputCaptureRule;
import static org.assertj.core.api.Assertions.assertThat;

public class MyTest {

    @Rule
    public OutputCaptureRule output = new OutputCaptureRule();

    @Test
    public void test() {
        System.out.println("Hello");
        assertThat(output.toString()).contains("Hello");
    }
}

New (JUnit 5 with OutputCaptureExtension):

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.test.system.CapturedOutput;
import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(OutputCaptureExtension.class)
class MyTest {

    @Test
    void test(CapturedOutput output) {
        System.out.println("Hello");
        assertThat(output).contains("Hello");
    }
}

Usage Pattern

import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.test.system.CapturedOutput;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(OutputCaptureExtension.class)
class OutputTest {

    @Test
    void testConsoleOutput(CapturedOutput output) {
        System.out.println("Standard output");
        System.err.println("Error output");
        
        // Combined output
        assertThat(output.getAll())
            .contains("Standard output")
            .contains("Error output");
        
        // Separate streams
        assertThat(output.getOut()).contains("Standard output");
        assertThat(output.getErr()).contains("Error output");
        
        // CharSequence methods
        assertThat(output).contains("Standard");
        assertThat(output).startsWith("Standard");
    }
}

Common Patterns

Pattern: Testing Logging

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ExtendWith(OutputCaptureExtension.class)
class LoggingTest {

    private static final Logger log = LoggerFactory.getLogger(LoggingTest.class);

    @Test
    void testLogOutput(CapturedOutput output) {
        log.info("Info message");
        log.error("Error message");
        
        assertThat(output)
            .contains("Info message")
            .contains("Error message");
    }
}

Pattern: Testing with Spring Boot

import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@ExtendWith(OutputCaptureExtension.class)
class SpringBootOutputTest {

    @Autowired
    private MyComponent component;

    @Test
    void testComponentOutput(CapturedOutput output) {
        component.execute();
        assertThat(output).contains("Execution completed");
    }
}

Pattern: Asserting No Output

@Test
void testSilentOperation(CapturedOutput output) {
    silentService.execute();
    assertThat(output.getAll()).isEmpty();
}

Pattern: Multi-Line Output

@Test
void testMultiLineOutput(CapturedOutput output) {
    System.out.println("Line 1");
    System.out.println("Line 2");
    System.out.println("Line 3");

    assertThat(output).contains("Line 1", "Line 2", "Line 3");

    String[] lines = output.getAll().split("\n");
    assertThat(lines).hasSize(3);
}

Pattern: Testing Exception Stack Traces

@Test
void testExceptionOutput(CapturedOutput output) {
    try {
        throw new RuntimeException("Test exception");
    } catch (RuntimeException e) {
        e.printStackTrace();
    }

    assertThat(output.getErr())
        .contains("RuntimeException")
        .contains("Test exception")
        .contains("at ");
}

Pattern: Pattern Matching

@Test
void testOutputPattern(CapturedOutput output) {
    System.out.println("User ID: 12345");
    System.out.println("Transaction ID: abc-def-ghi");

    assertThat(output)
        .containsPattern("User ID: \\d+")
        .containsPattern("Transaction ID: [a-z-]+");
}

Pattern: Output Ordering Verification

@Test
void testOutputOrder(CapturedOutput output) {
    System.out.println("Step 1: Initialize");
    System.out.println("Step 2: Process");
    System.out.println("Step 3: Finalize");

    String allOutput = output.getAll();
    assertThat(allOutput.indexOf("Step 1"))
        .isLessThan(allOutput.indexOf("Step 2"));
    assertThat(allOutput.indexOf("Step 2"))
        .isLessThan(allOutput.indexOf("Step 3"));
}

Pattern: Testing Banner Output

import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@ExtendWith(OutputCaptureExtension.class)
class BannerOutputTest {

    @Test
    void testSpringBootBanner(CapturedOutput output) {
        assertThat(output)
            .contains("Spring Boot")
            .contains("Started");
    }
}

Pattern: Log Level Testing

@Test
void testLogLevels(CapturedOutput output) {
    Logger logger = LoggerFactory.getLogger("test");

    logger.trace("Trace message");
    logger.debug("Debug message");
    logger.info("Info message");
    logger.warn("Warning message");
    logger.error("Error message");

    // Check which levels were logged
    String out = output.getAll();
    assertThat(out).contains("Info message");
    assertThat(out).contains("Warning message");
    assertThat(out).contains("Error message");
}

Pattern: Async Output Capture

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

@Test
void testAsyncOutput(CapturedOutput output) throws Exception {
    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
        System.out.println("Async output");
    });

    future.get(5, TimeUnit.SECONDS);

    assertThat(output).contains("Async output");
}

Pattern: Stream Output

@Test
void testStreamOutput(CapturedOutput output) {
    List.of("Item 1", "Item 2", "Item 3")
        .forEach(System.out::println);

    assertThat(output)
        .contains("Item 1")
        .contains("Item 2")
        .contains("Item 3");
}

Pattern: Formatted Output

@Test
void testFormattedOutput(CapturedOutput output) {
    System.out.printf("Name: %s, Age: %d%n", "Alice", 30);
    System.out.printf("Balance: $%.2f%n", 1234.567);

    assertThat(output)
        .contains("Name: Alice, Age: 30")
        .contains("Balance: $1234.57");
}

Pattern: Testing Component Output

@SpringBootTest
@ExtendWith(OutputCaptureExtension.class)
class ComponentOutputTest {

    @Autowired
    private ReportGenerator reportGenerator;

    @Test
    void testReportGeneration(CapturedOutput output) {
        reportGenerator.generateReport();

        assertThat(output)
            .contains("Report started")
            .contains("Processing records")
            .contains("Report completed");
    }
}

Pattern: Progress Indicator Testing

@Test
void testProgressIndicator(CapturedOutput output) {
    for (int i = 0; i <= 100; i += 20) {
        System.out.print("\rProgress: " + i + "%");
    }
    System.out.println();

    assertThat(output).contains("Progress: 100%");
}

Pattern: Testing ANSI Color Codes

@Test
void testColoredOutput(CapturedOutput output) {
    String red = "\u001B[31m";
    String reset = "\u001B[0m";

    System.out.println(red + "Error message" + reset);

    assertThat(output).contains("Error message");
    assertThat(output.getAll()).contains("\u001B[31m");
}

Pattern: Empty Stream Verification

@Test
void testNoStderr(CapturedOutput output) {
    System.out.println("Only stdout");

    assertThat(output.getOut()).isNotEmpty();
    assertThat(output.getErr()).isEmpty();
}

@Test
void testNoStdout(CapturedOutput output) {
    System.err.println("Only stderr");

    assertThat(output.getOut()).isEmpty();
    assertThat(output.getErr()).isNotEmpty();
}

Pattern: Charset and Encoding

@Test
void testUnicodeOutput(CapturedOutput output) {
    System.out.println("Unicode: \u2713 \u2717 \u263A");
    System.out.println("Emoji: \uD83D\uDE80 \uD83C\uDF89");

    assertThat(output)
        .contains("\u2713")
        .contains("\u263A");
}

Pattern: Large Output Testing

@Test
void testLargeOutput(CapturedOutput output) {
    for (int i = 0; i < 1000; i++) {
        System.out.println("Line " + i);
    }

    assertThat(output).contains("Line 0");
    assertThat(output).contains("Line 999");
    assertThat(output.getAll().split("\n")).hasSize(1000);
}

Pattern: Conditional Logging

@Test
void testConditionalLogging(CapturedOutput output) {
    Logger logger = LoggerFactory.getLogger("test");

    if (logger.isDebugEnabled()) {
        logger.debug("Debug message");
    }
    logger.info("Info message");

    assertThat(output).contains("Info message");
}

Pattern: Testing Startup Logs

@SpringBootTest
@ExtendWith(OutputCaptureExtension.class)
class StartupLoggingTest {

    @Test
    void testApplicationStartup(CapturedOutput output) {
        assertThat(output)
            .contains("Starting")
            .contains("Started")
            .contains("Tomcat started on port");
    }
}

Pattern: Multiple Tests with Separate Capture

@ExtendWith(OutputCaptureExtension.class)
class MultipleTestsOutputTest {

    @Test
    void firstTest(CapturedOutput output) {
        System.out.println("First test output");
        assertThat(output).contains("First test");
    }

    @Test
    void secondTest(CapturedOutput output) {
        System.out.println("Second test output");
        // Each test gets fresh CapturedOutput
        assertThat(output).contains("Second test");
        assertThat(output).doesNotContain("First test");
    }
}

Pattern: Suppressing Specific Output

@Test
void testFilteredOutput(CapturedOutput output) {
    System.out.println("Important message");
    System.out.println("DEBUG: internal detail");
    System.out.println("Another important message");

    // Verify important messages present
    assertThat(output)
        .contains("Important message")
        .contains("Another important message");

    // Optionally verify debug present (or filter it out in production)
    assertThat(output).contains("DEBUG: internal detail");
}

Pattern: Testing Print Methods

@Test
void testPrintMethods(CapturedOutput output) {
    System.out.print("No newline");
    System.out.print(" - ");
    System.out.println("With newline");

    assertThat(output).contains("No newline - With newline");
}

Pattern: Verification with AssertJ Extractors

@Test
void testWithExtractors(CapturedOutput output) {
    System.out.println("Status: SUCCESS");
    System.out.println("Duration: 1234ms");

    assertThat(output.getAll())
        .asString()
        .contains("SUCCESS")
        .containsPattern("Duration: \\d+ms");
}

Troubleshooting

Issue: Output not captured

  • Cause: Logging framework redirects output
  • Solution: Configure logging to use System.out/err

Issue: Output from before test captured

  • Cause: Capture starts at test method boundary
  • Solution: This is expected behavior

Issue: NullPointerException on output parameter

  • Cause: Forgot @ExtendWith annotation
  • Solution: Add @ExtendWith(OutputCaptureExtension.class)

See Also

  • Integration Testing - Full context testing
  • Context Runners - Isolated testing