How to test drive a main method

Test driven development is all nice and dandy, but there are some areas that most people find notoriously difficult to test, and often dismiss them as not worth it. The main() method, entry point for any application, is one of them. The problem with main methods is that they combine at least two of the patterns that make testing difficult: static method calls and many constructor calls. Let me illustrate it with a snippet of code from Thermostat:

public static void main(String[] args) {
    CommandContextFactory cmdCtxFactory = CommandContextFactory.getInstance();
    CommandRegistry registry = cmdCtxFactory.getCommandRegistry();
    ServiceLoader cmds = ServiceLoader.load(Command.class);
    registry.registerCommands(cmds);
    if (hasNoArguments()) {
        runHelpCommand();
    } else {
        runCommandFromArguments();
    }
}

This is a rather common way to implement a main method: setup a bunch of things by calling factories and constructors, then launch whatever needs to be launched.

The first thing that needs to be done to make this testable is to reduce the problematic static method and constructor calls by moving them into a separate class. How does this look?

public static void main(String[] args) {
    new Launcher().run(args);
}

This way, we can now test the bulk of the functionality using fairly normal testing techniques such as mocking, injection, etc. We still want to test the actual main method though (even though many will now cry out that this is so trival that it’s not worth even thinking about it… but wait, the solution is not so difficult either).

What would need to be tested here? Obviously, there is no state change, only interactions. Therefore, the test would need to be a verification of interaction of the main class with the Launcher class. More specifically, it would need to verify that the run() method is called on the newly constructed Launcher with the correct arguments (and not, say, null). However, we need to somehow get rid of the construction itself and open a way to inject the Launcher instance. That’s how I solved it:

public class Thermostat {

    private static Launcher launcher = new Launcher();

    public static void main(String[] args) {
        launcher.run(args);
    }

    static void setLauncher(Launcher launcher) {
        Thermostat.launcher = launcher;
    }
}

This way, we can override the launcher to be tested with a mock launcher, and verify the correct interaction:

public class ThermostatTest {

    @Test
    public void testThermostatMain() {
        Launcher launcher = Mockito.mock(Launcher.class);
        Thermostat.setLauncher(launcher);
        Thermostat.main(new String[] { "test1", "test2" });
        PowerMockito.verifyNew(Launcher.class).withNoArguments();
        Mockito.verify(launcher).run(new String[] { "test1", "test2" });
     }
}

The verifyNew() call verifies that we actually create a new instance of Launcher in the static initializer of Thermostat (the main class), and the last call verifies that run() is called on our mock launcher with the correct arguments.

And if you are interested in how the rest of the launching code now looks like, and how we test it (in fairly normal unit testing style), take a look at the Launcher and LauncherTest classes.

If you have other solutions to the problem of unit testing main methods, I would be very interested in hearing from them in the comments!

Advertisements

One Response to How to test drive a main method

  1. Ah yes, good thinking.

    I’m still trying to drag someone kicking and screaming into the joy of unit testing on my team, but would have dismissed your main() case as probably too much trouble if I could test all the bits underneath it…

    Rgds

    Damon

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: