In one of the projects I’m currently working on, I have to write a parser to translate a scripting language into Java code. This tutorial shows how you can unit test the generated Java code.
Let’s assume we have JavaCodeGeneratorService that looks something like this:-
public class JavaCodeGeneratorService {
public String generate(String packageName, String className) {
return "package " + packageName + ";" +
"public class " + className + " {" +
" public boolean isOne(Integer i) {" +
" return i == 1;" +
" }" +
"}";
}
}
This service class generates Java source code as one big String. To unit test this code, we have to:-
- Programmatically compile the generated source code.
- Instantiate the class and invoke isOne(…) to get the returned value.
So, the strategy here is to create an in-memory Java file object so that we don’t have to perform any clean ups. Then, we use reflection to invoke that API to get the returned value.
package com.choonchernlim.epicapp;
import com.choonchernlim.epicapp.service.JavaCodeGeneratorService;
import org.apache.log4j.Logger;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import org.junit.Test;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Locale;
public class JavaCodeGeneratorServiceTest {
// in-memory Java file object
class InMemoryJavaFileObject extends SimpleJavaFileObject {
private String contents = null;
public InMemoryJavaFileObject(String className, String contents)
throws Exception {
super(URI.create("string:///" + className.replace('.', '/') +
Kind.SOURCE.extension), Kind.SOURCE);
this.contents = contents;
}
public CharSequence getCharContent(boolean ignoreEncodingErrors)
throws IOException {
return contents;
}
}
private static Logger log = Logger.getLogger(SomeTest.class);
JavaCodeGeneratorService javaCodeGeneratorService = new JavaCodeGeneratorService();
@Test
public void testGeneratedCode() throws Exception {
String packageName = "com.choonchernlim.epicapp.service.impl";
String className = "HelloWorld";
compile(className, javaCodeGeneratorService.generate(packageName, className));
assertThat(invokeMethod(packageName, className, 1), is(true));
assertThat(invokeMethod(packageName, className, 2), is(false));
assertThat(invokeMethod(packageName, className, 0), is(false));
assertThat(invokeMethod(packageName, className, -1), is(false));
}
// creates an in-memory Java file object and compile it
private void compile(String className, String code) throws Exception {
// Create an in-memory Java file object
JavaFileObject javaFileObject = new InMemoryJavaFileObject(className, code);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,
null,
null);
// If the location of the new class files is not specified, it will be
// placed at the project's root directory. Since this is a Maven project,
// we will comply to the Maven structure and place the generated classes
// under `target/classes` directory. Otherwise, we may get
// `ClassNotFoundException` when we do the reflection.
Iterable<file> files = Arrays.asList(new File("target/classes"));
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, files);
// When shit happens, the `DiagnosticCollector` may reveal useful error messages
DiagnosticCollector<javafileobject> diagnostics = new DiagnosticCollector<javafileobject>();
JavaCompiler.CompilationTask task = compiler.getTask(null,
fileManager,
diagnostics,
null,
null,
Arrays.asList(javaFileObject));
boolean success = task.call();
fileManager.close();
// If there' a compilation error, display error messages and fail the test
if (!success) {
for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
log.fatal("Code: " + diagnostic.getCode());
log.fatal("Kind: " + diagnostic.getKind());
log.fatal("Position: " + diagnostic.getPosition());
log.fatal("Start Position: " + diagnostic.getStartPosition());
log.fatal("End Position: " + diagnostic.getEndPosition());
log.fatal("Source: " + diagnostic.getSource());
log.fatal("Message: " + diagnostic.getMessage(Locale.getDefault()));
}
fail("Compilation failed!");
}
}
// executes in-memory Java file object with input value
private boolean invokeMethod(String classPackage, String className, Integer input)
throws Exception {
Class<!--?--> clazz = Class.forName(classPackage + "." + className);
return (Boolean) clazz.getDeclaredMethod("isOne", Integer.class)
.invoke(clazz.newInstance(), input);
}
}
Leave a Reply