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.
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 :-).
EDIT: I meant ApplicationComposers is final in older versions, not ApplicationComposer. Note the missing S.
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
Hi Romain – We are in the process of moving from Web Logic to tomEE – We were able to deploy the WARs successfully using tomEE 1.7.4 and am in the process of deploying them on tomEE 7.0.5 (and possibly 7.1.0).
1. When I attempt with either tommEE 7.x versions, I get the following error –
org.apache.tomee.catalina.TomcatWebAppBuilder.startInternal Unable to deploy collapsed
ear in war StandardEngine[Catalina].StandardHost[localhost].StandardContext[/fdicServices]
java.lang.IllegalArgumentException: The com.jpmorgan.gcrm.security.esa.utils.PropertyUtilityTool is not annotated
the annotation error that I receive is from a class whose source code I do not have access to –
it’s in JAR that my app inherits
2. Just to get around the problem in 1 for time being, I commented out reference to the above offending class but am running into another issue –
org.apache.tomee.catalina.TomcatWebAppBuilder.startInternal Unable to deploy collapsed ear
in war StandardEngine[Catalina].StandardHost[localhost].StandardContext[/fdicServices]
org.apache.openejb.OpenEJBException: Can’t find resource for class ABC (No provider
available for resource-ref ‘null’ of type CLASS for ‘fdicServices.Comp292470356’.)
Any pointers you can give will be greatly appreciated!
Hello Ranji,
you should likely send a mail to users@tomee.apache.org but like that it can either be that a not EE module is scanned and reuses @Resource (scan.xml can solve it ensuring it is excluded from EE scanning) or a resource definition in tomee.xml is missing.