A single TomEE ApplicationComposer instance for all your tests!


TomEE ApplicationComposer is a nice solution for embedded EE testing. The goal is to describe its application in Java and deploy this model. However it starts OpenEJB and deploys/undeploys the application either by class or method depending the setup.

When this feature can be insane for small deployments which would benefit of an insanely easy configuration and simple mock solution it can be an issue for application deploying again and again the same model.

To avoid that the coming TomEE 7.0.0-M2 provides a new JUnit runner but you can already benefit from it with a single class!

The idea is pretty simple and consists of 3 main steps:

  • find the model: TomEE will try to find a single class annotated with @com.github.rmannibucau.jwp.runner.Application or will use the class set as system property to tomee.application-composer.application.
  • start and deploy the model for the first test and register a shutdown hook to undeploy and stop the container
  • for each test do injections to keep the smooth ApplicationComposer features

From ApplicationComposerRunner to SingleContainerRunner

The TomEE implementation looks like (you can just copy paste this class to get it before next tomee release):

import org.apache.openejb.core.ThreadContext;
import org.apache.openejb.testing.ApplicationComposers;
import org.apache.openejb.testing.RandomPort;
import org.apache.webbeans.config.WebBeansContext;
import org.apache.webbeans.inject.OWBInjector;
import org.apache.xbean.finder.AnnotationFinder;
import org.apache.xbean.finder.archive.FileArchive;
import org.junit.rules.MethodRule;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;

import java.lang.reflect.Field;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import static org.apache.openejb.loader.JarLocation.jarLocation;

// goal is to share the same container for all embedded tests and hold the config there
// only works if all tests use the same config
public class SingleContainerRunner extends BlockJUnit4ClassRunner {
    private static volatile boolean started = false;
    private static final AtomicReference<Object> APP = new AtomicReference<>();
    private static final AtomicReference<Thread> HOOK = new AtomicReference<>();

    public static void setApp(final Object o) {
        APP.set(o);
    }

    public static void close() {
        final Thread hook = HOOK.get();
        if (hook != null) {
            hook.run();
            Runtime.getRuntime().removeShutdownHook(hook);
            HOOK.compareAndSet(hook, null);
            APP.set(null);
        }
    }

    public SingleContainerRunner(final Class<?> klass) throws InitializationError {
        super(klass);

        if (APP.get() == null) {
            final Class<?> type;
            final String typeStr = System.getProperty("tomee.application-composer.application");
            if (typeStr != null) {
                try {
                    type = Thread.currentThread().getContextClassLoader().loadClass(typeStr);
                } catch (final ClassNotFoundException e) {
                    throw new IllegalArgumentException(e);
                }
            } else {
                final Iterator<Class<?>> descriptors =
                    new AnnotationFinder(new FileArchive(Thread.currentThread().getContextClassLoader(), jarLocation(klass)), false)
                        .findAnnotatedClasses(Application.class).iterator();
                if (!descriptors.hasNext()) {
                    throw new IllegalArgumentException("No descriptor class using @Application");
                }
                type = descriptors.next();
                if (descriptors.hasNext()) {
                    throw new IllegalArgumentException("Ambiguous @Application: " + type + ", " + descriptors.next());
                }
            }
            try {
                APP.compareAndSet(null, type.newInstance());
            } catch (final InstantiationException | IllegalAccessException e) {
                throw new IllegalStateException(e);
            }
        }
    }

    @Override
    protected List<MethodRule> rules(final Object test) {
        final List<MethodRule> rules = super.rules(test);
        rules.add((base, method, target) -> new Statement() {
            @Override
            public void evaluate() throws Throwable {
                start();
                OWBInjector.inject(WebBeansContext.currentInstance().getBeanManagerImpl(), target, null);
                composerInject(target);
                base.evaluate();
            }

            private void start() throws Exception {
                if (!started) {
                    final Object app = APP.get();
                    new ApplicationComposers(app.getClass()) {
                        @Override
                        public void deployApp(final Object inputTestInstance) throws Exception {
                            super.deployApp(inputTestInstance);
                            if (!started) {
                                final ThreadContext previous = ThreadContext.getThreadContext(); // dont here for logging
                                final ApplicationComposers comp = this;
                                final Thread hook = new Thread() {
                                    @Override
                                    public void run() {
                                        try {
                                            comp.after();
                                        } catch (final Exception e) {
                                            ThreadContext.exit(previous);
                                            throw new IllegalStateException(e);
                                        }
                                    }
                                };
                                HOOK.set(hook);
                                Runtime.getRuntime().addShutdownHook(hook);
                                started = true;
                            }
                        }
                    }.before(app);
                }
            }
        });
        return rules;
    }

    private void composerInject(final Object target) throws IllegalAccessException {
        final Object app = APP.get();
        final Class<?> aClass = target.getClass();
        for (final Field f : aClass.getDeclaredFields()) {
            if (f.isAnnotationPresent(RandomPort.class)) {
                for (final Field field : app.getClass().getDeclaredFields()) {
                    if (field.getType() ==  f.getType()) {
                        if (!field.isAccessible()) {
                            field.setAccessible(true);
                        }
                        if (!f.isAccessible()) {
                            f.setAccessible(true);
                        }

                        final Object value = field.get(app);
                        f.set(target, value);
                        break;
                    }
                }
            } else if (f.isAnnotationPresent(Application.class)) {
                if (!f.isAccessible()) {
                    f.setAccessible(true);
                }
                f.set(target, app);
            }
        }
        final Class<?> superclass = aClass.getSuperclass();
        if (superclass != Object.class) {
            composerInject(superclass);
        }
    }
}

A simple test with SingleContainerRunner

Once you get this runner you just set it on your test:

@RunWith(SingleContainerRunner.class)
public class UserResourceTest {
    @Inject
    private UserTransaction ut;

    @PersistenceContext
    private EntityManager em;

    @RandomPort("http")
    private URL base;

    @Test
    public void myTest() {
        // do some test
    }
}

You can see that the test doesn’t know anything of the application compared to a standard ApplicationComposer test but that it still uses EE injections and ApplicationComposer injections (@RandomPort).

ApplicationComposer model

The last part is to create our application model. It is like any ApplicationComposer model but in its own class:

@Default // app composer one, not cdi one
@SimpleLog
@Jars("deltaspike-")
@PersistenceUnitDefinition
@EnableServices(jaxrs = true)
@Classes(cdi = true, context = "app")
@Application // mark it as the model to use
public class AppDescriptor {
    @RandomPort("http")
    private URL base;
}

Important: we inject the URL base there cause this is the way the runner will be able to re-wire it in the tests.

Faster tests

This small change we were able to do since now ApplicationComposer allows to deploy applications without a test (see ApplicationComposers.run()) brings a lot to the application development. It makes the execution of your test suite really faster which encourages you to run it more often and to fix tests faster.

Advertisements

3 thoughts on “A single TomEE ApplicationComposer instance for all your tests!

  1. Rubin

    In older versions of TomEE/OpenEJB, ApplicationComposer is a final class which fouls up the anonymous class invocation ( new ApplicationComposers(app.getClass()) ).

    I’m trying out this cool stuff you’ve posted here with OpenEJB 4.7.4. I’ve managed to take out the Java 7 specific diamond notation stuff and the Java 8 lambda expression so far, but the fact that ApplicationComposers is final in this version of OpenEJB and not in the newer version afaics (https://github.com/apache/tomee/blob/master/container/openejb-core/src/main/java/org/apache/openejb/testing/ApplicationComposers.java) makes me wonder if/how to work around this.

    Help much appreciated :-).

    Reply
    1. rmannibucau Post author

      it is likely doable using openejb @Observes to match server events and do the same but it is a bit more complicated. personally i’d use 7.x since it shouldn’t break the app

      Reply

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