Unit testing Java main() methods

Most of the code I write does not go into Java main() methods and I’m quite accustomed to testing code in “normal” methods.  Recently, however, I came across a situation where I needed to confirm a defect, verify a fix, and perform additional testing of a handful of edge cases related to the functionality of a main() method.  In this post I describe the solution used.

First, some disclaimers to help define the scope and also recognize that I’m doing a couple things that I would normally best practices:

  • Since the main() method is usually the entry to the entire application, running it normally involves a lot of code.  In this case I want to only test the functionality of the code in the main() method itself and not the functionality of stuff that it calls–these are real unit tests.
  • There are already many unit tests in place using JUnit so I want to continue using JUnit and be able to run everything together.
  • Generally I think it’s poor form to to modify code to make it testable–code that is well-written should already be easy to test.  However, I occasionally make exceptions such as making a method protected instead of private in order to expose it to testing (I think that’s a lot cleaner than using reflection to run a private method).

So, here’s a very simplified example of the class with which I started (note that the non-main methods are not implemented):

package com.nathanbak.test.main;

public class Application {

    protected Application() { }

    public static void main(String[] args) {
        String string = args[0];
        Application application = new Application();
        try {
            application.run(string);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            System.exit(1);
        }
        System.exit(0);
    }
    
    protected void run(String string) throws Exception { }
    
    protected void logException() { }
}

I try to avoid putting much code in main() methods, but typically end up having things like:

  • Parsing/validating arguments (something the above example does poorly)
  • Object instantiation
  • Running of object methods (often with arguments)
  • Providing some sort of top level exception handling and user output
  • Provide a relevant exit code (typically 0 for success and 1 for failure)

When I tried creating JUnits for the main() method, here are some problems I encountered:

  1. If a test called the main() method, the test run would halt because of the System.exit() call.
  2. Verifying output to System.out is difficult.
  3. Running the main() method means running the run() method which can take a long time and changes the scope of the test.
  4. It’s hard to test edge cases (such as what if the run() method throws an exception that doesn’t have any message).

I was worried that #1 was going to be a deal breaker, but fortunately found an easy solution in System Rules which bills itself as “A collection of JUnit rules for testing code that uses java.lang.System.”  I added the system-rules-1.16.0.jar to my classpath, followed the examples on the main page, and had a great solution to #1 and #2 above (as well as solutions to various other problems I hadn’t considered but will keep in mind for the future).

To cover #3 and #4 above was where I had to get a bit more unorthodox.  I created a private static method that creates and returns the new object and replaced the constructor in the main() method with a call to that new method.  I also added a protected static property that I could use to store an instance and had my new static method use that instance if it was not null.  This is now my class turned out:

package com.nathanbak.test.main;

public class Application {

    protected Application() { }

    public static void main(String[] args) {
        String string = args[0];
        Application application = newApplication();
        try {
            application.run(string);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            System.exit(1);
        }
        System.exit(0);
    }
    
    protected void run(String string) throws Exception { }
    
    protected void logException() { }

    protected static Application instance = null;
    
    private static Application newApplication() {
        return instance == null ? new Application() : instance;
    }
}

With the ability to control the object used to run methods, I was able to create tests like:

package com.nathanbak.test.main;

import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.ExpectedSystemExit;

public class ApplicationTest extends Application {

    @After
    public void tearDown() throws Exception {
        instance = null;
    }
    
    @Rule
    public final ExpectedSystemExit exit = ExpectedSystemExit.none();

    @Test
    public void testMain() {
        instance = new CrashAndBurn();
        String [] args = { "one", "two", "three" };
        exit.expectSystemExitWithStatus(1);
        main(args);
    }
    
    private static class CrashAndBurn extends Application {
        @Override
        protected void run(String string) throws Exception { 
            throw new Exception();
        }
    }
}

A few things to note about the test class:

  • The test class needs to extend the class under test in order to have access to the instance property.
  • The testMain() test currently calls main with some args and then the CrashAndBurn class throws an exception when the run() method is called and the test confirms that the main() method returns an exit code of 1 (the test would fail if any other exit code was returned).
  • The tearDown() method sets the instance back to null after every test to ensure that specific instances are only used when desired for specific tests.

Obviously my real tests are more numerous and comprehensive, but the above example shows how I was able to overcome the difficulties encountered when trying to create unit tests for a Java main() method.

Author: Nathan

I like to do stuff.

Leave a Reply

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