Saturday, 29 June 2013

Handling command line arguments with args4j

From time to time, we need to write a tool that is using command line arguments as input.
Having an interface similar to unix command line tools is not a trivial task but with args4j it becomes quite easy.

We're going to write a fake tool called FileCompressor which process of invocation would look like:

compressor -i inputFile.txt -o outputFile.txt -p high
view raw invocation hosted with ❤ by GitHub
where -i is input file name, -o output file name and -p the priority of the process.

Let's start with defining the dependencies of our project:

<dependency>
<groupId>args4j</groupId>
<artifactId>args4j</artifactId>
<version>2.0.23</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
view raw dep.xml hosted with ❤ by GitHub
We'll need args4j, the rest is for testing.

Our fake FileCompressor requires a configuration that will be populated by args4j.

public class FileCompressor {
public void compressFile(Configuration configuration) {
System.out.println("Received: " + configuration);
}
}
The configuration object has fields that will be mapped to command line arguments with args4j annotations.

public class Configuration {
private enum Priority {
HIGH, LOW;
}
@Option(name = "-i", usage = "input file name", required = true)
private String inputFileName;
@Option(name = "-o", usage = "output file name", required = true)
private String outputFileName;
@Option(name = "-p", usage = "process priority")
private Priority priority = Priority.HIGH;
public String getInputFileName() {
return inputFileName;
}
public String getOutputFileName() {
return outputFileName;
}
public Priority getPriority() {
return priority;
}
@Override
public String toString() {
return "Configuration{" +
"inputFileName='" + inputFileName + '\'' +
", outputFileName='" + outputFileName + '\'' +
", priority=" + priority +
'}';
}
}
And here is the class that will be invoked:

public class App {
private static Configuration configuration = new Configuration();
private static FileCompressor fileCompressor = new FileCompressor();
private static CmdLineParser parser = new CmdLineParser(configuration);
private static PrintStream outputStream = System.out;
public static void main(String[] args) {
try {
parser.parseArgument(args);
fileCompressor.compressFile(configuration);
} catch (CmdLineException e) {
outputStream.println(e.getMessage());
parser.printUsage(outputStream);
}
}
}
view raw App.java hosted with ❤ by GitHub
First of all, the arguments are parsed and configuration object is populated with them.
If there is a parsing problem, the exception is printed along with the usage (args4j can automatically print the usage of our application based on used annotations).

Testing our application requires some effort.
To begin with, we would like to mock completely the FileCompressor. There is no setter for it so we'll use Spring's ReflectionTestUtils together with Mockito.

@RunWith(MockitoJUnitRunner.class)
public class AppTest {
private static final String INPUT_FILE = "input.txt";
private static final String OUTPUT_FILE = "output.txt";
private static final String INVALID_ARGUMENT = "-www";
@Mock
private FileCompressor fileCompressor;
private App app = new App();
@Before
public void setUp() {
setField(app, "fileCompressor", fileCompressor);
}
view raw AppTest.java hosted with ❤ by GitHub
We've also prepared some test data.

Let's check if the configuration object is populated properly and if the collabolator was invoked:

@Test
public void shouldPopulateConfiguration() {
// when
app.main((String[]) asList("-i", INPUT_FILE, "-o", OUTPUT_FILE).toArray());
// then
Configuration configuration = (Configuration) getField(app, "configuration");
assertThat(configuration.getInputFileName(), is(INPUT_FILE));
assertThat(configuration.getOutputFileName(), is(OUTPUT_FILE));
}
@Test
public void shouldInvokeFileCompressor() {
// when
app.main((String[]) asList("-i", INPUT_FILE, "-o", OUTPUT_FILE).toArray());
// then
verify(fileCompressor).compressFile(isA(Configuration.class));
}
view raw AppTest1.java hosted with ❤ by GitHub
We would also like to know if the usage was printed when invalid arguments were passed.
To do that we need to use a spy, as we want to have the original behavior of populating argument, but we want to verify if printUsage method was invoked.

@Test
public void shouldPrintUsageWhenInvalidArguments() {
// given
Configuration configuration = (Configuration) getField(app, "configuration");
CmdLineParser parser = spy(new CmdLineParser(configuration));
setField(app, "parser", parser);
String[] invalidArguments = (String[]) asList(INVALID_ARGUMENT, INPUT_FILE).toArray();
// when
app.main(invalidArguments);
// then
verify(parser).printUsage(isA(PrintStream.class));
}
view raw AppTest2.java hosted with ❤ by GitHub
The last thing to check is if the exception was printed to the output stream when invalid arguments were passed.
Similarly we need to use a spy.

@Test
public void shouldPrintExceptionOnOutputStreamWhenInvalidArguments() {
// given
PrintStream outputStream = spy(System.out);
setField(app, "outputStream", outputStream);
String[] invalidArguments = (String[]) asList(INVALID_ARGUMENT, INPUT_FILE).toArray();
// when
app.main(invalidArguments);
// then
verify(outputStream).println("\"" + INVALID_ARGUMENT + "\" is not a valid option");
}
view raw AppTest3.java hosted with ❤ by GitHub
The whole project can be found at github.