Tuesday, March 1, 2011

Unit testing Java's System.out output

This topic came up in CS222, and the resulting legwork may of value to others. I posted a similar message privately to the course discussion forum.


The problem is that the students are writing console applications, but we're also learning about the value of test-driven development. You can unit test console apps much more easily than GUI, but it requires a bit of I/O sleight-of-hand. Your application writes to System.out by definition, and so you need to be able to capture the program's output somehow. There is a System.setOut method that can be used to change where System.out.print* writes. This method requires a PrintStream, and so one solution is to wrap a ByteArrayOutputStream in a PrintStream.


Consider the following example:




public class Exemplar {

    public static void main(String[] args) {
        System.out.println("Num args: " + args.length);
    }
    
}


This program simply will prints the number of arguments sent on the command line. In Eclipse, you can specify the arguments through the Run Configurations dialog.


The unit test for this looks like the following:


import static org.junit.Assert.assertEquals;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;

import org.junit.Test;

public class ExemplarTest {
    @Test
    public void testMain() throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        System.setOut(new PrintStream(baos));
        Exemplar.main(new String[] { "foo" });
        baos.flush();
        String whatWasPrinted = new String(baos.toByteArray());
        String[] linesOfOutput = whatWasPrinted.split(//
                System.getProperty("line.separator"));
        assertEquals(1, linesOfOutput.length);
        assertEquals("Num args: 1", linesOfOutput[0]);
    }

}


There are three tricks here that may require elucidation.
  1. It's possible for a buffered IO class to cache its result in memory before writing it to a stream. This is a good thing, since writing to a stream is expensive and writing to memory is cheap. However, this can mean that the results you're expecting to see in your stream may not show up when you go looking for them. The call to baos.flush will flush the buffers to the stream so you know the content will be there when we go reading for it. In my opinion, it's not worth remembering exactly which classes require flushing: it's polite to just flush anyway.
  2. You can make a String out of a byte array. It's generally not a good idea due to character encoding complications. In this case, however, we know it's really character data in the array since we're putting it there ourselves via System.out.print commands.
  3. We would like this to be cross-platform, but different operating systems handle end-of-line characters differently. Java gives us a system property for divining this information, and so we can use this to split the String into multiple lines.

No comments:

Post a Comment