October 23, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Test-Driving a Java Command Line Application

  • April 3, 2007
  • By Jeff Langr
  • Send Email »
  • More Articles »

Imagine a typical application class in Java, one exposing a main() method:

package unrefactored;

public class App {
   private final String server;
   private final int portNumber;
   private boolean wasSomethingDone;

   public static void main(String[] args) {
      if (args.length < 2) {
         System.out.println("port must be supplied");
         System.out.println("Usage:ntjava App server port");
         System.exit(1);
      }
      if (args.length > 2) {
         System.out.println("too many arguments");
         System.out.println("Usage:ntjava App server port");
         System.exit(1);
      }

      String server = args[0];
      String port = args[1];
      int portNumber = -1;
      try {
         portNumber = Integer.parseInt(port);
      }
      catch (NumberFormatException e) {
         System.out.println("Usage:ntjava App server port");
         System.exit(1);
      }

      new App(server, portNumber).execute();
      System.exit(0);
   }

   public App(String server, int portNumber) {
      this.server = server;
      this.portNumber = portNumber;
   }

   public void execute() {
      System.out.printf("server=%s; port=%s%n", server, portNumber);
      ...
   }
}

Looks typical, right? What would it take to unit-test this code? Should we write unit tests for it? The main method will certainly get tested in the course of manual application verification. It's also something that might seem hard to write tests for. Why should we bother?

Of course, I have some reasons why I think it's worth the effort. You can read those at the end of the article. In the meantime, I think your reason to bother is that you're doing test-driven development (TDD). You want to drive the existence of all lines of code in your production application by virtue of a failing bit of unit test code.

Where to Start?

When doing TDD, you want to build your code incrementally. That suggests that your starting point should involve the simplest logic you can think of that you can easily prove. Once you get that working, you will continue to follow the pattern of determining the next simplest step.

What's the simplest possible case for which you can write useful code? How about when the user has supplied an insufficient number of arguments to the application?

Here's your first test, something that you can code in a couple of minutes.

import static org.junit.Assert.*;
import java.io.*;
import org.junit.Test;

public class AppTest {
   private static final String EOL =
      System.getProperty("line.separator");

   @Test
   public void testShowUsageWhenInsufficientArgumentsSupplied() {
      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
      PrintStream console = System.out;
      try {
         System.setOut(new PrintStream(bytes));
         App.main();
      } finally {
         System.setOut(console);
      }
      assertEquals(String.format(
            "too few arguments%n" +
            "Usage:ntjava App server port%n", EOL, EOL),
            bytes.toString());
   }
}

Unfortunately, this doesn't compile. The call to main expects a String[] of arguments. You could change the call to:

App.main(new String[] {});

Or better, you can change the signature of main to support varargs:

public static void main(String... args) {
}

Getting this one test to pass requires a couple lines of code in main:

public class App {
   public static void main(String... args) {
      System.out.println("too few arguments");
      System.out.println("Usage:ntjava App server port");
   }
}

Not even an if clause! No tests yet exist to require one. Don't worry, you'll correct that soon enough.

The bulk of the code in testShowUsageWhenInsufficientArgumentsSupplied redirects console output to an in-memory byte stream. That allows the application to use System.out.println statements. A better solution, however, might be to require the application code to send its output to a PrintStream object. This would allow substitution of the console with any other PrintStream, which is a more flexible design anyway. For now, you'll stick with System.out.

The try-finally block in the test is important. Without it, System.out might remain set to the ByteArrayOutputStream for the duration of the test run. Any test failures or exceptions thrown during execution of the test would bypass the statement that resets System.out to the actual console object.

Moving Along

A second test can verify what happens when too many arguments are supplied. This second test has much in common with the first, so you can refactor to a more elegant set of tests:

import static org.junit.Assert.*;
import java.io.*;

import org.junit.*;

public class AppTest {
   private static final String EOL =
      System.getProperty("line.separator");
   private PrintStream console;
   private ByteArrayOutputStream bytes;

   @Before
   public void setUp() {
      bytes   = new ByteArrayOutputStream();
      console = System.out;
      System.setOut(new PrintStream(bytes));
   }

   @After
   public void tearDown() {
      System.setOut(console);
   }

   @Test
   public void testAbortWhenInsufficientArgumentsSupplied() {
      App.main();
      assertEquals(App.MSG_TOO_FEW_ARGUMENTS + EOL +
                   App.USAGE + EOL, bytes.toString());
   }

   @Test
   public void testAbortWhenTooManyArgumentsSupplied() {
      App.main("a", "b", "c");
      assertEquals(App.MSG_TOO_MANY_ARGUMENTS + EOL +
                   App.USAGE + EOL, bytes.toString());
   }
}

The @Before and @After methods manage the diversion of console output. JUnit 4 guarantees that code in @After executes subsequent to each test.

The resulting production code exposes constants. String literals that appear in both test and production (target) classes represent duplication that we must eliminate.

public class App {
   static final String USAGE = "Usage:ntjava App server port";
   static final String MSG_TOO_FEW_ARGUMENTS  = "too few arguments";
   static final String MSG_TOO_MANY_ARGUMENTS = "too many arguments";

   public static void main(String... args) {
      if (args.length < 2) {
         System.out.println(MSG_TOO_FEW_ARGUMENTS);
      } else if (args.length > 2)
         System.out.println(MSG_TOO_MANY_ARGUMENTS);
      System.out.println(USAGE);
   }
}

The next simplest possible test is one that demonstrates failure upon trying to convert the supplied port to an integer.

@Test
public void testAbortWhenPortNotInteger() {
   App.main("server", "port not a number");
   assertEquals(App.MSG_PORT_MUST_BE_NUMBER + EOL + App.USAGE + EOL,
                consoleText.toString());
}
Note: The renaming of the bytes to the more expressive consoleText. Returning to this exercise a day after producing the previous code, I felt that the name bytes wasn't helpful.

The code in App.java that makes the new test pass:

public class App {
   static final String USAGE = "Usage:ntjava App server port";
   static final String MSG_TOO_FEW_ARGUMENTS   = "too few arguments";
   static final String MSG_TOO_MANY_ARGUMENTS  = "too many arguments";
   static final String MSG_PORT_MUST_BE_NUMBER = "port not numeric";

   public static void main(String... args) {
      if (args.length < 2) {
         System.out.println(MSG_TOO_FEW_ARGUMENTS);
      } else if (args.length > 2)
         System.out.println(MSG_TOO_MANY_ARGUMENTS);
      else {
         String port = args[1];
         try {
            Integer.parseInt(port);
         } catch (NumberFormatException e) {
            System.out.println(MSG_PORT_MUST_BE_NUMBER);
         }
      }
      System.out.println(USAGE);
   }
}

Because you have solid code coverage with your tests, you can refactor the code at will. You can abstract the above conditional to a separate method:

   public static void main(String... args) {
      validate(args);
   }

   private static void validate(String[] args) {
      if (args.length < 2) {
         System.out.println(MSG_TOO_FEW_ARGUMENTS);
      } else if (args.length > 2)
         System.out.println(MSG_TOO_MANY_ARGUMENTS);
      else {
         String port = args[1];
         try {
            Integer.parseInt(port);
         } catch (NumberFormatException e) {
            System.out.println(MSG_PORT_MUST_BE_NUMBER);
         }
      }
      System.out.println(USAGE);
   }

The tests themselves can and absolutely should get refactored, as long as refactoring doesn't detract from the readability of the tests. You can eliminate the duplication that each of the assert statements exhibits with the others:

@Test
public void testAbortWhenInsufficientArgumentsSupplied() {
   App.main();
   assertAbort(App.MSG_TOO_FEW_ARGUMENTS);
}

@Test
public void testAbortWhenTooManyArgumentsSupplied() {
   App.main("a", "b", "c");
   assertAbort(App.MSG_TOO_MANY_ARGUMENTS);
}

@Test
public void testAbortWhenPortNotInteger() {
   App.main("server", "port not a number");
   assertAbort(App.MSG_PORT_MUST_BE_NUMBER);
}

private void assertAbort(String expectedMessage) {
   assertEquals(expectedMessage + EOL + App.USAGE + EOL,
                consoleText.toString());
}




Page 1 of 2



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel