gamelan
Search EarthWeb
CodeGuru | Gamelan | Jars | Wireless | Discussions
Navigate developer.com
Architecture & Design  
Database  
Java
Languages & Tools
Microsoft & .NET
Open Source  
Project Management  
Security  
Techniques  
Voice  
Web Services  
Wireless/Mobile
XML  
New
 
Technology Jobs  

   Developer.com Webcasts:
  The Impact of Coding Standards and Code Reviews

  Project Management for the Developer

  Defining Your Own Software Development Methodology

  more Webcasts...




Vote for the Developer.com Product of the Year Winners!




Developer Jobs

Be a Commerce Partner














 


Related Article -
Can Refactoring Produce Better Code?
Comments on Comments on Comments
Why Pair?: Challenges and Rewards of Pair Programming
Java 5's DelayQueue
Pair Programming Dos and Don'ts
A Brief Introduction to Agile
Writing a Simple Automated Test in FitNesse
Moving Forward with Automated Acceptance Testing
The Need for Automated Acceptance Testing
Developer News -
Microsoft's WinHec Doesn't Match Buzz of PDC    November 10, 2008
Are We Ready for the Cloud?    November 7, 2008
Windows 7 Drivers to Get a Makeover    November 6, 2008
Sun Serves Up Some Java EE 6 in GlassFish    November 6, 2008
Free Tech Newsletter -

Test-Driving a Java Command Line Application
By Jeff Langr

Go to page: Prev  1  2  

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.

Go to page: Prev  1  2  


Tools:
Add www.developer.com to your favorites
Add www.developer.com to your browser search box
IE 7 | Firefox 2.0 | Firefox 1.5.x
Receive news via our XML/RSS feed


Enterprise Java Archives






internet.comearthweb.comDevx.commediabistro.comGraphics.com

Search:

Jupitermedia Corporation has two divisions: Jupiterimages and JupiterOnlineMedia

Jupitermedia Corporate Info

Legal Notices, Licensing, Reprints, Permissions, Privacy Policy.
Advertise | Newsletters | Tech Jobs | Shopping | E-mail Offers