Java/Kotlin Unit Testing – Test Stdout/Stderr/Logs
Note: see also my old blog post if you want a more complex solution 😉 It targets at replacing the log appender. The below described method is simpler (nice!), it just listens in on ALL standard output+errors.
Note: if you want a “Kotest” version, you can look at a great blog post by Tomas Zezula here: Streamlining Console Output Verification with Kotest
When using spring-boot unit testing (library spring-boot-test-2.6.2.jar or newer), you can capture the output as send to the standard out and standard error channels. This allows for example testing output to log statements (by sending logs to stdout / console appender).
From the JavaDocs:
package org.springframework.boot.test.system;
// JUnit Jupiter @Extension to capture System.out and System.err. Can be registered for an entire
// test class or for an individual test method via @ExtendWith. This extension provides parameter
// resolution for a CapturedOutput instance which can be used to assert that the correct output
// was written.
// To use with @ExtendWith, inject the CapturedOutput as an argument to your test class
// constructor, test method, or lifecycle methods:
@ExtendWith(OutputCaptureExtension.class)
class MyTest {
@Test
void test(CapturedOutput output) {
System.out.println("ok");
assertThat(output).contains("ok");
System.err.println("error");
}
@AfterEach
void after(CapturedOutput output) {
assertThat(output.getOut()).contains("ok");
assertThat(output.getErr()).contains("error");
}
}
A Kotlin example:
import org.springframework.boot.test.system.OutputCaptureExtension
import org.hamcrest.CoreMatchers.containsString
@ExtendWith(OutputCaptureExtension::class)
class SomeTest {
@Test
fun `the test method`(output: CapturedOutput) {
// ... do something here which logs info ...
// and check expected output
assertThat(output.all, containsString("expected output"))
}
}
Ok, stop reading here if you can use above examples 😉
I was curious as to HOW spring does this capturing, and you can simplify their implementation with bare minimal functionality to something like this (if you are not using spring for example):
Java example:
// Attach a buffer to stdout
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
PrintStream originalStdout = System.out; // same possible for System.err
System.setOut(new PrintStream(buffer)); // same possible for System.setErr()
// --------------------------------------------------------------------------------
// Test sending data (note: it does not echo to stdout, it is going to our buffer)
// This can also be done by a log-appender sending it to stdout / console
System.out.println("test");
// --------------------------------------------------------------------------------
// Restore to normal stdout channel
System.setOut(originalStdout);
// Do something with captured data
System.out.println("Captured: " + buffer.toString());
That’s all there is to it 😉 Just alter the Std-Out to a buffer, and back at the end.
Or you can try a more complex example, which DOES echo the data to the console while also putting it in a buffer:
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import lombok.SneakyThrows;
public class Test {
// This main us used in this example instead of a normal test class.
public static void main(String[] args) {
CaptureStdOutAndStderr capture = new CaptureStdOutAndStderr();
System.out.println("test");
System.out.println("hello");
System.err.println("error");
// Restore normal output
capture.close();
System.out.println("captured:\n" + capture.getOutput());
System.out.println(capture.contains("hello"));
}
public static class CaptureStdOutAndStderr extends PrintStream {
private static PrintStream parentStdout = null;
private static PrintStream parentStdErr = null;
private static final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public String getOutput() {
return buffer.toString();
}
public boolean contains(String search) {
return buffer.toString().contains(search);
}
public CaptureStdOutAndStderr() {
super(buffer);
if (parentStdout == null) {
buffer.reset();
parentStdout = System.out;
parentStdErr = System.err;
System.setOut(this);
System.setErr(this);
}
}
@Override
public void close() {
super.close();
if (parentStdout != null) {
System.setOut(parentStdout);
System.setOut(parentStdErr);
parentStdout = null;
parentStdErr = null;
}
}
@SneakyThrows
@Override
public void write(int b) {
write(new byte[] { (byte) (b & 0xFF) });
}
@Override
public void write(byte[] b, int off, int len) {
buffer.write(b, off, len);
parentStdout.write(b, off, len);
}
@Override
public void flush() {
parentStdout.flush();
}
}
}
Note: this is just a hacky example to explain what sort of things you could try, nothing really serious. This would need some refactoring / cleaning. It has a nasty mix between static and non-static, no error handling, and puts std-out and std-err on one big pile.