NOTES INDEX This page is part of the collection of "notes" - these can be considered to be micro-blogs.
Java/Kotlin Unit Testing – Test Stdout/Stderr/Logs

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.

February 3, 2022

Leave a Reply

Your email address will not be published. Required fields are marked *