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.
    1. 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);
          }
      }
      

      Advertisements

One thought on “Java: Programmatically Compile and Unit Test Generated Java Source Code

  1. Pingback: Java: Programmatically Compile and Unit Test Generated Groovy Source Code | My Shitty Code

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s