Java library for integration testing with Docker containers
Log and output stream management for containers. Provides multiple consumer implementations for capturing, processing, and analyzing container output streams with support for ANSI code filtering and various output sinks.
Represents a single frame of container output with timestamp and type information.
/**
* Represents a frame of output from a container.
* Contains the output bytes, type (stdout/stderr), and timestamp.
*/
public class OutputFrame {
/**
* Constructor for creating an OutputFrame from output type and bytes.
*
* @param type the output type (STDOUT, STDERR, or END)
* @param bytes the output bytes
*/
public OutputFrame(OutputType type, byte[] bytes);
/**
* Create an OutputFrame from a docker-java Frame.
*
* @param frame the docker-java Frame object
* @return OutputFrame instance
*/
public static OutputFrame forFrame(Frame frame);
/**
* Get the output as a UTF-8 string.
*
* @return output as UTF-8 string
*/
public String getUtf8String();
/**
* Get the output as a UTF-8 string without the trailing line ending.
*
* @return output without line ending
*/
public String getUtf8StringWithoutLineEnding();
/**
* Get the raw output bytes.
*
* @return raw byte array
*/
public byte[] getBytes();
/**
* Get the type of output (stdout, stderr, or end marker).
*
* @return output type
*/
public OutputType getType();
/**
* Static constant representing end of stream marker.
* Use this to check for or signal stream completion.
*/
public static final OutputFrame END;
/**
* Output type enumeration.
*/
public enum OutputType {
/** Standard output */
STDOUT,
/** Standard error */
STDERR,
/** End of stream marker */
END;
/**
* Create an OutputType from a docker-java StreamType.
*
* @param streamType the docker-java StreamType
* @return corresponding OutputType
*/
public static OutputType forStreamType(StreamType streamType);
}
}Usage Example:
import org.testcontainers.containers.output.OutputFrame;
import java.util.function.Consumer;
// Process output frames
Consumer<OutputFrame> frameProcessor = frame -> {
switch (frame.getType()) {
case STDOUT:
System.out.println("[OUT] " + frame.getUtf8String());
break;
case STDERR:
System.err.println("[ERR] " + frame.getUtf8String());
break;
case END:
System.out.println("Stream ended");
break;
}
};Abstract base class for implementing output consumers with ANSI code filtering support.
/**
* Base class for output consumers providing common functionality.
* By default, ANSI escape codes are removed from output (removeColorCodes defaults to true).
* Note: Internal field is named removeColorCodes, but the method parameter is removeAnsiCodes.
*
* @param <SELF> Self-referential generic type for fluent API
*/
public abstract class BaseConsumer<SELF extends BaseConsumer<SELF>>
implements Consumer<OutputFrame> {
/**
* Configure whether to remove ANSI escape codes from output.
* Defaults to true (ANSI codes are removed by default).
*
* @param removeAnsiCodes true to remove ANSI codes, false to keep them
* @return this consumer for method chaining
*/
public SELF withRemoveAnsiCodes(boolean removeAnsiCodes);
/**
* Get whether ANSI escape codes are removed from output.
* Lombok @Getter generates this method from the removeColorCodes field.
*
* @return true if ANSI codes are removed, false if kept
*/
public boolean isRemoveColorCodes();
/**
* Set whether to remove ANSI escape codes from output.
* Lombok @Setter generates this method from the removeColorCodes field.
*
* @param removeColorCodes true to remove ANSI codes, false to keep them
*/
public void setRemoveColorCodes(boolean removeColorCodes);
/**
* Accept and process an output frame.
*
* @param frame the output frame to process
*/
@Override
public abstract void accept(OutputFrame frame);
}Send container output to an SLF4J logger for integration with application logging.
/**
* Consumer that sends container output to an SLF4J logger.
* By default, all output (both STDOUT and STDERR) is logged at INFO level.
* When separateOutputStreams is true, STDOUT is logged at INFO level and STDERR at ERROR level.
*
* Prefix formatting behavior:
* - With prefix and separateOutputStreams: format is "[prefix] : {message}"
* - With prefix and without separateOutputStreams: format is "[prefix] {outputType}: {message}"
* - Without prefix and separateOutputStreams: just "{message}"
* - Without prefix and without separateOutputStreams: "{outputType}: {message}"
* Note: Prefixes are automatically wrapped in brackets with a trailing space.
*/
public class Slf4jLogConsumer extends BaseConsumer<Slf4jLogConsumer> {
/**
* Create a log consumer with the specified logger.
*
* @param logger the SLF4J logger to use
*/
public Slf4jLogConsumer(Logger logger);
/**
* Create a log consumer with separate output stream handling.
*
* @param logger the SLF4J logger to use
* @param separateOutputStreams true to log stdout to INFO and stderr to ERROR
*/
public Slf4jLogConsumer(Logger logger, boolean separateOutputStreams);
/**
* Add a prefix to all log messages.
* The prefix will be automatically wrapped in brackets with a trailing space: "[prefix] "
*
* @param prefix the prefix string (will be formatted as "[prefix] ")
* @return this consumer for method chaining
*/
public Slf4jLogConsumer withPrefix(String prefix);
/**
* Add an MDC (Mapped Diagnostic Context) entry.
*
* @param key MDC key
* @param value MDC value
* @return this consumer for method chaining
*/
public Slf4jLogConsumer withMdc(String key, String value);
/**
* Add multiple MDC (Mapped Diagnostic Context) entries.
*
* @param mdc map of MDC entries
* @return this consumer for method chaining
*/
public Slf4jLogConsumer withMdc(Map<String, String> mdc);
/**
* Enable separate output streams (stdout to INFO, stderr to ERROR).
*
* @return this consumer for method chaining
*/
public Slf4jLogConsumer withSeparateOutputStreams();
@Override
public Slf4jLogConsumer withRemoveAnsiCodes(boolean removeAnsiCodes);
@Override
public void accept(OutputFrame frame);
}Usage Examples:
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.utility.DockerImageName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// Send container logs to application logger
Logger logger = LoggerFactory.getLogger("container.postgres");
GenericContainer<?> postgres = new GenericContainer<>(DockerImageName.parse("postgres:15"))
.withExposedPorts(5432)
.withLogConsumer(new Slf4jLogConsumer(logger));
postgres.start();
// Container output is logged via SLF4J
// With ANSI code filtering
Logger appLogger = LoggerFactory.getLogger("container.app");
GenericContainer<?> app = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
.withExposedPorts(8080)
.withLogConsumer(new Slf4jLogConsumer(appLogger)
.withRemoveAnsiCodes(true)); // Remove color codes
// Use logger name based on container
GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.0"))
.withExposedPorts(6379)
.withLogConsumer(new Slf4jLogConsumer(
LoggerFactory.getLogger("testcontainers.redis")));Collect container output into a string for later analysis or assertions.
/**
* Consumer that collects output into a string buffer.
* Useful for capturing output for assertions in tests.
*/
public class ToStringConsumer extends BaseConsumer<ToStringConsumer> {
/**
* Get the collected output as a UTF-8 string.
*
* @return all collected output
*/
public String toUtf8String();
/**
* Get the collected output with a specific charset.
*
* @param charset the charset to use for decoding
* @return all collected output decoded with the specified charset
*/
public String toString(Charset charset);
@Override
public ToStringConsumer withRemoveAnsiCodes(boolean removeAnsiCodes);
@Override
public void accept(OutputFrame frame);
}Usage Examples:
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.utility.DockerImageName;
// Collect all output for assertion
ToStringConsumer consumer = new ToStringConsumer();
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("alpine:latest"))
.withCommand("sh", "-c", "echo 'Hello World' && echo 'Test Output'")
.withLogConsumer(consumer);
container.start();
// Wait for container to finish
Thread.sleep(1000);
// Assert on collected output
String output = consumer.toUtf8String();
assertTrue(output.contains("Hello World"));
assertTrue(output.contains("Test Output"));
// Collect output without ANSI codes
ToStringConsumer cleanConsumer = new ToStringConsumer()
.withRemoveAnsiCodes(true);
GenericContainer<?> colorfulApp = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
.withLogConsumer(cleanConsumer);
colorfulApp.start();
String cleanOutput = cleanConsumer.toUtf8String();
// Output has ANSI escape codes removedWait for specific output patterns or stream completion with timeout support.
Note: WaitingConsumer does not provide a toUtf8String() method for retrieving captured output. After waiting for conditions, use container.getLogs() to retrieve the complete container output, or use ToStringConsumer if you need to capture output as a string.
/**
* Consumer that waits for specific output patterns or stream completion.
* Useful for synchronizing test execution with container events.
*/
public class WaitingConsumer extends BaseConsumer<WaitingConsumer> {
/**
* Get access to the underlying frame buffer.
*
* @return the LinkedBlockingDeque containing all output frames
*/
public LinkedBlockingDeque<OutputFrame> getFrames();
/**
* Wait indefinitely until a predicate matches an output frame.
*
* @param predicate condition to wait for
* @throws TimeoutException if timeout is reached
*/
public void waitUntil(Predicate<OutputFrame> predicate) throws TimeoutException;
/**
* Wait until a predicate matches an output frame, with timeout.
*
* @param predicate condition to wait for
* @param limit timeout value
* @param limitUnit timeout unit
* @throws TimeoutException if timeout is reached
*/
public void waitUntil(Predicate<OutputFrame> predicate, int limit, TimeUnit limitUnit)
throws TimeoutException;
/**
* Wait until a predicate matches an output frame N times.
*
* @param predicate condition to wait for
* @param limit timeout value
* @param limitUnit timeout unit
* @param times number of times the predicate must match
* @throws TimeoutException if timeout is reached
*/
public void waitUntil(Predicate<OutputFrame> predicate, long limit, TimeUnit limitUnit, int times)
throws TimeoutException;
/**
* Wait until the output stream ends.
* Waits indefinitely (Long.MAX_VALUE nanoseconds).
*/
public void waitUntilEnd();
/**
* Wait until the output stream ends, with timeout.
*
* @param limit timeout value
* @param limitUnit timeout unit
* @throws TimeoutException if timeout is reached
*/
public void waitUntilEnd(long limit, TimeUnit limitUnit) throws TimeoutException;
@Override
public WaitingConsumer withRemoveAnsiCodes(boolean removeAnsiCodes);
@Override
public void accept(OutputFrame frame);
}Usage Examples:
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.WaitingConsumer;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
// Wait for specific log message
WaitingConsumer waitingConsumer = new WaitingConsumer();
GenericContainer<?> app = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
.withExposedPorts(8080)
.withLogConsumer(waitingConsumer);
app.start();
// Wait for application to be ready
waitingConsumer.waitUntil(
frame -> frame.getUtf8String().contains("Application started"),
30,
TimeUnit.SECONDS
);
// Now safe to proceed with tests
String appUrl = String.format("http://%s:%d", app.getHost(), app.getMappedPort(8080));
// Wait for specific error message
WaitingConsumer errorWatcher = new WaitingConsumer();
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("test:latest"))
.withLogConsumer(errorWatcher);
container.start();
try {
errorWatcher.waitUntil(
frame -> frame.getType() == OutputFrame.OutputType.STDERR &&
frame.getUtf8String().contains("FATAL ERROR"),
10,
TimeUnit.SECONDS
);
fail("Expected fatal error did not occur");
} catch (TimeoutException e) {
// No error occurred - test passes
}
// Wait for container to finish execution
WaitingConsumer finishWatcher = new WaitingConsumer();
GenericContainer<?> oneShot = new GenericContainer<>(DockerImageName.parse("alpine:latest"))
.withCommand("sh", "-c", "sleep 5 && echo 'Done'")
.withLogConsumer(finishWatcher);
oneShot.start();
// Wait for output stream to end
finishWatcher.waitUntilEnd(10, TimeUnit.SECONDS);
// Container has finished execution
String logs = oneShot.getLogs();Adapter for integrating with docker-java's callback mechanism. Advanced users can use this for custom output handling integrations.
Note: Most users should use one of the higher-level consumer classes (Slf4jLogConsumer, ToStringConsumer, WaitingConsumer) or implement Consumer<OutputFrame> directly. FrameConsumerResultCallback is a lower-level API for advanced integration scenarios where you need direct control over the docker-java callback mechanism. Unless you have specific docker-java integration requirements, prefer the standard consumer implementations.
Important: GenericContainer.withLogConsumer() expects a Consumer<OutputFrame>, not a FrameConsumerResultCallback. To use multiple consumers, either combine them into a single Consumer<OutputFrame> (see Multi-Consumer Pattern example below) or call withLogConsumer() multiple times.
/**
* Docker-java callback adapter for frame consumers.
* Provides low-level access to container output streams.
* WARNING: This is an advanced class. Most users should use Slf4jLogConsumer,
* ToStringConsumer, or WaitingConsumer instead.
*/
public class FrameConsumerResultCallback
extends ResultCallbackTemplate<FrameConsumerResultCallback, Frame> {
/**
* Add a consumer for a specific output type.
*
* @param outputType the output type (STDOUT, STDERR, END)
* @param consumer the consumer to handle frames of this type
*/
public void addConsumer(OutputFrame.OutputType outputType, Consumer<OutputFrame> consumer);
/**
* Get the completion latch for waiting until output stream ends.
*
* @return CountDownLatch that counts down when stream completes
*/
public CountDownLatch getCompletionLatch();
/**
* Process the next frame from the docker-java stream.
*
* @param frame the next frame to process
*/
@Override
public void onNext(Frame frame);
/**
* Handle errors from the docker-java stream.
*
* @param throwable the error that occurred
*/
@Override
public void onError(Throwable throwable);
/**
* Close the callback and release resources.
*/
@Override
public void close();
}Choosing the Right Consumer - Decision Guide:
Use this decision tree to select the appropriate consumer for your use case:
| Use Case | Recommended Consumer | Reason |
|---|---|---|
| Log container output to SLF4J logger | Slf4jLogConsumer | Integrates with your logging framework, respects log levels |
| Capture all output as string for assertions | ToStringConsumer | Simple string capture, good for testing output content |
| Wait for specific log message before proceeding | WaitingConsumer | Blocks until condition met, perfect for synchronization |
| Process each output line as it arrives | Custom Consumer<OutputFrame> | Direct frame-by-frame processing, flexible |
| Advanced docker-java integration | FrameConsumerResultCallback | Low-level callback access, only when needed |
Quick Decision Flowchart:
Need to log to SLF4J/Logback/Log4j?
├─ Yes → Use Slf4jLogConsumer
└─ No
└─ Need to capture output for assertions?
├─ Yes → Use ToStringConsumer
└─ No
└─ Need to wait for specific output?
├─ Yes → Use WaitingConsumer
└─ No
└─ Need custom per-frame processing?
├─ Yes → Implement Consumer<OutputFrame>
└─ No → Need docker-java callback integration?
└─ Yes → Use FrameConsumerResultCallback (advanced)Common Patterns:
// Pattern 1: Debug logging during development
container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("container")));
// Pattern 2: Verify output in tests
ToStringConsumer toStringConsumer = new ToStringConsumer();
container.withLogConsumer(toStringConsumer);
container.start();
assertThat(toStringConsumer.toUtf8String()).contains("Server started");
// Pattern 3: Synchronize with container startup
WaitingConsumer waitingConsumer = new WaitingConsumer();
container.withLogConsumer(waitingConsumer);
container.start();
waitingConsumer.waitUntil(frame ->
frame.getUtf8String().contains("ready to accept connections"), 30, TimeUnit.SECONDS);
// Pattern 4: Multiple consumers for different purposes
container.withLogConsumer(new Slf4jLogConsumer(logger)) // Log everything
.withLogConsumer(toStringConsumer); // Also capture for assertionsAttach multiple consumers to process output in different ways simultaneously.
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.containers.output.WaitingConsumer;
import org.testcontainers.utility.DockerImageName;
import org.slf4j.LoggerFactory;
import java.util.function.Consumer;
public class MultiConsumerExample {
@Test
public void testWithMultipleConsumers() throws Exception {
// Create multiple consumers
Slf4jLogConsumer slf4jConsumer = new Slf4jLogConsumer(
LoggerFactory.getLogger("test.container"));
ToStringConsumer stringConsumer = new ToStringConsumer();
WaitingConsumer waitingConsumer = new WaitingConsumer();
// Combine consumers
Consumer<OutputFrame> multiConsumer = frame -> {
slf4jConsumer.accept(frame); // Log to SLF4J
stringConsumer.accept(frame); // Collect to string
waitingConsumer.accept(frame); // Enable waiting
};
GenericContainer<?> app = new GenericContainer<>(
DockerImageName.parse("myapp:latest"))
.withExposedPorts(8080)
.withLogConsumer(multiConsumer);
app.start();
// Wait for ready message
waitingConsumer.waitUntil(
frame -> frame.getUtf8String().contains("Server started"),
30,
TimeUnit.SECONDS
);
// Run tests
// ...
// Analyze collected output
String allOutput = stringConsumer.toUtf8String();
assertTrue(allOutput.contains("Server started"));
assertFalse(allOutput.contains("ERROR"));
}
}Implement a custom consumer for specialized output processing.
import org.testcontainers.containers.output.BaseConsumer;
import org.testcontainers.containers.output.OutputFrame;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
// Custom consumer that writes to a file
public class FileLogConsumer extends BaseConsumer<FileLogConsumer> {
private final PrintWriter writer;
private final DateTimeFormatter formatter;
public FileLogConsumer(String filename) throws IOException {
this.writer = new PrintWriter(new FileWriter(filename, true));
this.formatter = DateTimeFormatter.ISO_INSTANT;
}
@Override
public void accept(OutputFrame frame) {
if (frame.getType() != OutputFrame.OutputType.END) {
String timestamp = formatter.format(Instant.now());
String type = frame.getType().name();
String message = frame.getUtf8StringWithoutLineEnding();
writer.println(String.format("[%s] %s: %s", timestamp, type, message));
writer.flush();
}
}
public void close() {
writer.close();
}
}
// Custom consumer that collects errors
public class ErrorCollector extends BaseConsumer<ErrorCollector> {
private final List<String> errors = new ArrayList<>();
@Override
public void accept(OutputFrame frame) {
if (frame.getType() == OutputFrame.OutputType.STDERR) {
String message = frame.getUtf8StringWithoutLineEnding();
if (message.contains("ERROR") || message.contains("Exception")) {
errors.add(message);
}
}
}
public List<String> getErrors() {
return new ArrayList<>(errors);
}
public boolean hasErrors() {
return !errors.isEmpty();
}
}
// Usage
ErrorCollector errorCollector = new ErrorCollector();
FileLogConsumer fileLogger = new FileLogConsumer("container.log");
Consumer<OutputFrame> combined = frame -> {
errorCollector.accept(frame);
fileLogger.accept(frame);
};
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
.withLogConsumer(combined);
container.start();
// After test
assertFalse(errorCollector.hasErrors(),
"Container had errors: " + errorCollector.getErrors());
fileLogger.close();import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.WaitingConsumer;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
import java.util.regex.Pattern;
public class PatternBasedOutputTest {
@Test
public void testDatabaseMigration() throws Exception {
WaitingConsumer consumer = new WaitingConsumer();
GenericContainer<?> app = new GenericContainer<>(
DockerImageName.parse("myapp:latest"))
.withExposedPorts(8080)
.withEnv("RUN_MIGRATIONS", "true")
.withLogConsumer(consumer);
app.start();
// Wait for migration start
consumer.waitUntil(
frame -> frame.getUtf8String().contains("Starting database migrations"),
10,
TimeUnit.SECONDS
);
// Wait for migration completion with pattern matching
Pattern migrationPattern = Pattern.compile(".*Completed (\\d+) migrations.*");
consumer.waitUntil(
frame -> migrationPattern.matcher(frame.getUtf8String()).find(),
2,
TimeUnit.MINUTES
);
// Application is ready for testing
}
@Test
public void testWorkerScaling() throws Exception {
WaitingConsumer consumer = new WaitingConsumer();
GenericContainer<?> worker = new GenericContainer<>(
DockerImageName.parse("worker:latest"))
.withEnv("WORKERS", "3")
.withLogConsumer(consumer);
worker.start();
// Count how many workers started
int workersStarted = 0;
Pattern workerPattern = Pattern.compile(".*Worker (\\d+) ready.*");
for (int i = 0; i < 3; i++) {
consumer.waitUntil(
frame -> workerPattern.matcher(frame.getUtf8String()).find(),
10,
TimeUnit.SECONDS
);
workersStarted++;
}
assertEquals(3, workersStarted, "All workers should have started");
}
}import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.utility.DockerImageName;
public class DebuggingOutputTest {
@Test
public void testWithDetailedLogging() {
ToStringConsumer consumer = new ToStringConsumer();
GenericContainer<?> app = new GenericContainer<>(
DockerImageName.parse("myapp:latest"))
.withExposedPorts(8080)
.withEnv("DEBUG", "true")
.withEnv("LOG_LEVEL", "DEBUG")
.withLogConsumer(consumer);
try {
app.start();
// Run test that might fail
// ...
} catch (Exception e) {
// Dump container output for debugging
System.err.println("=== Container Output ===");
System.err.println(consumer.toUtf8String());
System.err.println("=== End Container Output ===");
throw e;
}
}
@Test
public void testWithFilteredOutput() {
// Only capture ERROR and WARN messages
ToStringConsumer errorConsumer = new ToStringConsumer();
Consumer<OutputFrame> filteringConsumer = frame -> {
String message = frame.getUtf8String();
if (message.contains("ERROR") || message.contains("WARN")) {
errorConsumer.accept(frame);
}
};
GenericContainer<?> container = new GenericContainer<>(
DockerImageName.parse("myapp:latest"))
.withLogConsumer(filteringConsumer);
container.start();
// Only errors and warnings are collected
String criticalMessages = errorConsumer.toUtf8String();
if (!criticalMessages.isEmpty()) {
System.err.println("Critical messages detected:");
System.err.println(criticalMessages);
}
}
}import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.utility.DockerImageName;
public class RealTimeMonitoringTest {
@Test
public void testWithRealTimeMonitoring() throws InterruptedException {
AtomicInteger lineCount = new AtomicInteger(0);
AtomicBoolean errorDetected = new AtomicBoolean(false);
Consumer<OutputFrame> monitor = frame -> {
lineCount.incrementAndGet();
String message = frame.getUtf8String();
System.out.println("[CONTAINER] " + message);
// Detect errors in real-time
if (message.contains("ERROR") || message.contains("Exception")) {
errorDetected.set(true);
System.err.println("!!! ERROR DETECTED: " + message);
}
// Trigger actions based on output
if (message.contains("Memory usage: 90%")) {
System.err.println("!!! HIGH MEMORY WARNING");
}
};
GenericContainer<?> app = new GenericContainer<>(
DockerImageName.parse("myapp:latest"))
.withExposedPorts(8080)
.withLogConsumer(monitor);
app.start();
// Give container time to produce output
Thread.sleep(5000);
// Check monitoring results
assertTrue(lineCount.get() > 0, "Container should have produced output");
assertFalse(errorDetected.get(), "No errors should be detected");
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-testcontainers--testcontainersdocs