Java: Programmatically Compile and Unit Test Generated Java Source Code

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:-

  1. Programmatically compile the generated source code.
  2. 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);
    }
}

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *