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); } }
One thought on “Java: Programmatically Compile and Unit Test Generated Java Source Code”