JavaEnterprise JavaTest-Driving a Java Command Line Application

Test-Driving a Java Command Line Application

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

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());
}

System.exit Challenges

The next part of writing a test is a bit more challenging. You need to verify that an appropriate error code is returned to the operating system. Right now, all three of the abort conditions should return an error code of 1. You can use the System.exit() method to accomplish this need. Unfortunately, calling System.exit() at any point will cause your JUnit run to fail as well.

Somehow, you need to override System.exit(), or otherwise intercept a call to it, capturing the argument passed to it. Unfortunately, it’s a static method. The most effective way is probably to encapsulate the call to System.exit() in an instance-side method, and override it in the test. Unfortunately again, you’re doing all this from the main method, which is itself static. Statics in Java just make for more difficult testing!

A bit of rethinking about what you are testing is in order. Your goal is really not to put a bunch of code in the main method and then validate it. Your real goal is to ensure that you can execute the application from the command line, and that you can verify that your application guards against invalid command line arguments. Your realization should be, then, that you don’t really need all that code in the main method itself.

Instead, you can put all of the initial logic into App instance-side methods. Your final goal, then, would be to verify that the main method initialized the logic defined in App.

Making the first part of the change is easy. In the test, you create a field of App type and initialize it in the @Before method. You also change invocations of App.main() to instance-side calls to app.execute().

...
public class AppTest {
   ...
   private App app;

   @Before
   public void setUp() {
      consoleText = new ByteArrayOutputStream();
      console = System.out;
      System.setOut(new PrintStream(consoleText));
      app = new App();
   }
   ...
   @Test
   public void testAbortWhenInsufficientArgumentsSupplied() {
      app.execute();
      assertAbort(App.MSG_TOO_FEW_ARGUMENTS);
   }

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

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

In the production code, you create execute() and change validate to be an instance-side method. You also delete main because it’s no longer tested. All tests still pass!

public class App {
   ...
   private void validate(String[] args) {
      ...
   }

   public void execute(String... args) {
      validate(args);
   }
}

Now, you’re ready to verify the return code. You create an override of the App class in the @Before method. This override supplies a definition for a to-be-created exit method. The override simply traps the error code passed to it.

public class AppTest {
   ...
   private int errorCode;

   @Before
   public void setUp() {
      consoleText = new ByteArrayOutputStream();
      console = System.out;
      System.setOut(new PrintStream(consoleText));
      app = new App() {
         @Override
         public void exit(int errorCode) {
            AppTest.this.errorCode = errorCode;
         }
      };
   }
   ...
   private void assertAbort(String expectedMessage) {
      assertEquals(expectedMessage + EOL + App.USAGE + EOL,
                   consoleText.toString());
      assertEquals(App.ERROR_CODE_BAD_ARGUMENTS, errorCode);
   }
}

Having refactored the individual tests to use assertAbort means that you were able to add this new assertion in one place!

The production code that gets this to pass:

public void execute(String... args) {
   validate(args);
   exit(ERROR_CODE_BAD_ARGUMENTS);
}

public void exit(int errorCode) {
   System.exit(errorCode);
}

The last statement in execute() delegates to the exit() method defined on App. In production, exit simply delegates to System.exit(), which terminates the application. When AppTest executes, it uses the overridden version of exit() instead.

A key point: this stubbing-out of exit implies that the real production code it contains is not actually getting tested. The implication is that you have some form of integration or system test that will verify whether or not the application works in a deployed environment. Unit testing is never a complete solution to testing!

Final Steps

You can quickly write a positive test against App, and then come back and figure out how to test-drive the main method.

@Test
public void testExecuteWhenArgumentsValid() {
   app.execute("server", "8080");
   assertEquals("server", app.server());
   assertEquals(8080, app.port());
   assertEquals(0, errorCode);
}

After adding the production code that makes this test pass, you now can refactor to a cleaner solution:

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";
   static final int ERROR_CODE_BAD_ARGUMENTS   = 1;

   private int port;
   private String server;
   private String errorMessage;

   private boolean validate(String[] args) {
      if (args.length < 2) {
         errorMessage = MSG_TOO_FEW_ARGUMENTS;
         return false;
      }
      if (args.length > 2) {
         errorMessage = MSG_TOO_MANY_ARGUMENTS;
         return false;
      }

      try {
         port = Integer.parseInt(args[1]);
      } catch (NumberFormatException e) {
         errorMessage = MSG_PORT_MUST_BE_NUMBER;
         return false;
      }

      server = args[0];
      return true;
   }

   public void execute(String... args) {
      if (!validate(args)) {
         System.out.println(errorMessage);
         System.out.println(USAGE);
         exit(ERROR_CODE_BAD_ARGUMENTS);
      }
   }

   public void exit(int errorCode) {
      System.exit(errorCode);
   }

   public String server() {
      return server;
   }

   public int port() {
      return port;
   }
}

The final test:

@Test
public void testExecuteFromMain() {
   App.main("server", "8080");
   App app = App.current();
   assertEquals("server", app.server());
   assertEquals(8080, app.port());
}

And the production code in App:

public class App {
   ...
   private static App app = null;

   public static App current() {
      return app;
   }

   public static void main(String... args) {
      app = new App();
      app.execute(args);
   }
   ...

The minor concession to testing is that the main() method stores the App instance in a static variable, and makes it available via the current() method.

Getting the App main class under test represented a bit of effort and some small convolutions. Is it worth it? Absolutely, in my experience. I’ve encountered many difficult main methods. Often, they contain a lot of ugly code to validate arguments. This ugly code easily masks defects. Sure, you can find open source libraries that can help simplify parsing of command-line args. But, you also can test-drive your own main methods with a reasonable amount of effort. You don’t have to give up on TDD.

Another reason to consider this approach is that it represents a more flexible design. Imagine a second class requiring a main method, or an entire suite of command-line applications. A reusable structure easily emerges from our finished code, and new command-line apps are even simpler to put together. New tests become easier to write, too!

About the Author

Jeff Langr is a veteran software developer with a score and more years of experience. He’s authored two books and dozens of published articles on software development, including Agile Java: Crafting Code With Test-Driven Development (Prentice Hall) in 2005. You can find out more about Jeff at his site, http://langrsoft.com, or you can contact him directly at jeff@langrsoft.com.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories