This post is part 3/5 of my Data-Driven Code Generation of Unit Tests series.
This blog post explains how I used Java, Apache Maven, StringTemplate, and JUnit to perform data-driven code generation of unit tests for a financial performance analytics library. If you haven’t read it already, I recommend starting with Part 1: Background.
As mentioned in Part 2: C++, CMake, Jinja2, Boost, all performance analytics metadata is stored in a single file called metadata.csv. This file drives all code generation and is what helps ensure inter-platform consistency.
In order to integrate code generation into the Maven build process, I was forced to create three separate Java projects:
A Maven project which implements the unit test generation using metadata.csv and StringTemplate (java-gentest-srcgen)
A Maven project which builds a Maven plugin which calls the unit test generator at the right point in the Maven build lifecycle (java-gentest-maven-plugin)
A Maven project which implements the calculations and uses the Maven plugin to generate the source code (java-lib)
All three projects are tied together using a single parent POM which looks like this:
The Java unit test generator project (java-gentest-srcgen) creates a JAR with a single class, JavaUnitTestGenerator, with a single public method generate(). This method parses metadata.csv and calls StringTemplate to generate a unit test class for each found calculation.
publicclassJavaUnitTestGenerator{// A simple logging interface defined in this project so we can integrate// into any logging system, including Maven'sprivateSimpleLoggerlogger;privateFiletargetDirectory;publicJavaUnitTestGeneratorsetLogger(SimpleLoggerlogger){this.logger=logger;returnthis;}publicJavaUnitTestGeneratorsetTargetDirectory(FiletargetDirectory){this.targetDirectory=targetDirectory;returnthis;}publicvoidgenerate()throwsException{// The Maven build process adds metadata.csv as a resource to// the JAR so that the JAR becomes self-containedtry(InputStreamis=getClass().getResourceAsStream("metadata.csv")){try(InputStreamReadersr=newInputStreamReader(is)){try(BufferedReaderbr=newBufferedReader(sr)){String[]headers=br.readLine().split(",");Stringline;while((line=br.readLine())!=null){String[]arr=line.split(",");Map<String,String>kvp=newHashMap<String,String>();for(inti=0;i<arr.length;++i){kvp.put(headers[i],arr[i]);}generateUnitTest(kvp);}}}}}privatevoidgenerateUnitTest(Map<String,String>kvp)throwsException{// Instantiate the templateURLurl=Resources.getResource("unit-test.stg");STGroupg=newSTGroupFile(url,"US-ASCII","<",">");STtemplate=g.getInstanceOf("unit_test");for(Map.Entry<String,String>entry:kvp.entrySet()){if(template.getAttributes().containsKey(entry.getKey())){// Don't pass in false values so that if(variable) evaluates to falseif(!entry.getValue().equals("false")){template.add(entry.getKey(),entry.getValue());}}}// Add various transformations and manipulations to the attributes// found in metadata.csv that cannot easily be done in StringTemplate's// templating languagetemplate.add("algorithm_type_pascal_case",toPascalCase(kvp.get("algorithm_type")));template.add("function_name_pascal_case",toPascalCase(kvp.get("function_name")));template.add("function_name_camel_case",toCamelCase(kvp.get("function_name")));...// Generate the source code fileFiletgtFile=newFile(targetDirectory,toPascalCase(kvp.get("function_name"))+"UnitTest.java");logger.info("Generating "+tgtFile+"...");try(FileOutputStreamos=newFileOutputStream(tgtFile)){try(OutputStreamWriterosw=newOutputStreamWriter(os)){STWriterstWriter=newAutoIndentWriter(osw);template.write(stWriter);}}}}
The template itself (unit-test.stg) looks something like:
groupjavagentest;unit_test(algorithm_type,algorithm_type_pascal_case,function_name,...)::=<<packagecom.morningstar.perfanalytics.tests;...publicclass<function_name_pascal_case>UnitTest{// BEGIN ARRAY TESTS@TestpublicvoidtestArrayUnannualized(){....doubleexpected=<expected_value_unannualized>;doubleactual=...;assertEquals(expected,actual,0.00001);}....>>
Unfortunately, because StringTemplate’s templating language is so weak, we’re stuck with the following limitations:
The JavaUnitTestGenerator class must perform a number of presentation-oriented transformations on attributes found in metadata.csv, such as case conversions or creating small Java snippets. This means that the JavaUnitTestGenerator and the template are extremely tightly coupled in non-obvious ways.
The lack of a for-loop construct in the template language means we cannot perform tricks like we did in Part 2, where we generate all possible combinations of annualization, frequency, etc. for a given calculation.
Maven plugin
The Maven plugin project creates a maven-plugin that wraps the unit test source code generator. The source code looks like:
@Mojo(name="generate",requiresProject=true,threadSafe=false,requiresDependencyResolution=ResolutionScope.COMPILE_PLUS_RUNTIME,defaultPhase=LifecyclePhase.GENERATE_SOURCES)publicclassJavaUnitTestCodegenMojoextendsAbstractMojo{@Parameter(defaultValue="${project}")privateMavenProjectproject;@Parameter(property="outputDirectory",defaultValue="${project.build.directory}/generated-test-sources/unit-tests")privateFileoutputDirectory;@Overridepublicvoidexecute()throwsMojoExecutionException{getLog().info("Generating unit tests for Java calculation library");// MojoLogger adapts Maven's logging class to the SimpleLogger// interface in the java-gentest-srcgen projectJavaUnitTestGeneratorg=newJavaUnitTestGenerator().setLogger(newMojoLogger(getLog())).setTargetDirectory(this.outputDirectory);try{g.generate();}catch(Exceptionex){thrownewMojoExecutionException("Error generating source code",ex);}project.addTestCompileSourceRoot(outputDirectory.getPath());}}
Note how the plugin instructs the calling project to add the generated source code directory to the test compile source root. This way, the calling project doesn’t need to remember to configure this in its pom.xml.
Calculation library
The calculation library contains the implementation of all calculations. It references the Maven plugin project in its pom.xml in order to have the Maven plugin create its unit tests as part of the build process. The relevant stanza looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
<build><plugins><!-- Generate unit test code --><plugin><groupId>com.morningstar.perfanalytics</groupId><artifactId>perfanalytics-java-gentest-maven-plugin</artifactId><version>...</version><executions><execution><goals><goal>generate</goal></goals></execution></executions></plugin></plugins></build>
The Java library also has a few other, hand-written unit tests, in the normal directory (src/test/java/...). The build system knows how to compile and run both the hand-written and the machine-generated unit tests at build time.
I found this approach reliable, and mostly straightforward, but with a few drawbacks. First, the unit test generator, as coded, will generate the source code every time, even if nothing has changed. This means we will perform a lot of unnecessary re-compilation, slowing down builds. Second, weaknesses in the templating language make it frustrating to use; I have to frequently switch back-and-forth between the template file and the Java code to achieve my desired result. Third, it’s unfortunate that we have to write a custom Maven plugin just to perform source code generation at build time; it’d be nice if there was a simpler way. Fourth, the versions of all the Maven projects must be kept in sync at all times; fortunately, the Maven release plugin does this for us automatically.
Next time I’ll talk about doing this in C#, MSBuild, T4 Text Templates, and the Microsoft Unit Test Framework for managed code!