I'm currently working on writing some tests for a java EE project that uses guice for dependency injection. Dealing with the entity manager and it's datasource has been giving me no end of trouble. I have tried multiple different approaches and they all seem to have a fatal flaw somewhere. I can't seem to figure out how to work with InitialContext without it cross contaminating my tests.
My first attempt was to manually bind the datasource into the context in the guice provider method for my datasource. Something like:
private static Context context;
@Provides
@Singleton
public DataSource getNavDS() throws NamingException {
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.naming.java.javaURLContextFactory");
System.setProperty(Context.URL_PKG_PREFIXES, "org.apache.naming");
BasicDataSource ds = new BasicDataSource();
ds.setUrl("jdbc:h2:mem:myDB;create=true;MODE=MSSQLServer;DATABASE_TO_UPPER=FALSE;");
if( context == null)
{
context = new InitialContext();
context.createSubcontext("java:");
context.createSubcontext("java:comp");
context.createSubcontext("java:comp/env");
context.createSubcontext("java:comp/env/jdbc");
}
context.rebind(NAV_DS, ds);
return ds;
}
It's a bit dirty, but as long as I reset the datasource between each test it works ok for unit tests. The problem is, I also need to do integration tests with an embedded jetty server. The integration tests have their own guice module, but if I make a call to new InitialContext()
it will end up giving me the instance created during the previous tests. That means I can't create a test suite because my unit tests have contaminated the Context before the integration tests run.
My next attempt was to try using a factory to mock the context lookup as outlined here: How to fake InitialContext with default constructor . So something like:
public class TestContextFactory implements InitialContextFactory {
private static final String NAV_DS = "java:comp/env/jdbc/NavDS";
@Override
public Context getInitialContext(Hashtable<?, ?> environment) throws NamingException {
final BasicDataSource ds = new BasicDataSource();
ds.setUrl("jdbc:h2:mem:myDB;create=true;MODE=MSSQLServer;DATABASE_TO_UPPER=FALSE;");
Context context = Mockito.mock(Context.class);
Mockito.when(context.lookup(NAV_DS)).thenReturn(ds);
return context;
}
}
The problem is, other code (like the entity manager) is also making calls against my mocked context. I would end up having to mock every method in InitialContext to avoid exceptions.
I also made an attempt to just not use the context in my provider method. I'm not actually doing any manual lookups in my tests, so I theory I should just be able to initialize and return the datasource. That was also a failure. The entity manager is expecting a datasource to be bound. If I don't bind my datasource into a context, the entity manager will throw an exception during initialization.
At this point I'm out of ideas. If anyone has a better way to deal with this (other than simply mocking everything) I would love to hear it. Thanks.
In the process of researching another idea for this problem, I finally found a partial solution here: Junit Testing JNDI InitialContext outside the application server
The key insight was being able to 'reset' the context between tests. Once I had that working, I was able to cleanup my context between tests. Doing that removed the need for mocking out the Context; I could use a real implementation and be confident about the state it was in at the start of each test.
The finished version of the test module ended up looking like this:
private static Context context;
private static final String NAV_DS = "java:comp/env/jdbc/NavDS";
//using a provider method here so we can inject the context into the test
@Provides
public Context getContext() throws NamingException
{
if( context == null)
{
context = new InitialContext();
context.createSubcontext("java:comp/env");
context.createSubcontext("java:comp/env/jdbc");
}
return context;
}
@Provides
@Singleton
public DataSource getNavDS() throws NamingException {
BasicDataSource ds = null;
ds = new BasicDataSource();
ds.setUrl("jdbc:h2:mem:myDB;create=true;MODE=MSSQLServer;DATABASE_TO_UPPER=FALSE;");
Context context = getContext();
context.rebind(NAV_DS, ds);
return ds;
}
and the context factory was changed to:
public class TestContextFactory implements InitialContextFactory {
private static final ThreadLocal<Context> currentContext = new ThreadLocal<Context>();
@Override
public Context getInitialContext(Hashtable<?, ?> environment) throws NamingException {
return currentContext.get();
}
public static void setCurrentContext(Context context) {
currentContext.set(context);
}
public static void clearCurrentContext() {
currentContext.remove();
}
}
Take note that since I am using a different jndi implementation (jetty rather than tomcat) I had to modify the context setup to match.
Once that was done, I just needed code in my tests to actually initialize and reset the context. I went with a JUnit rule as per the example, but you could accomplish the same thing with @Before and @After methods. The rule ended up looking like this:
public class MockInitialContextRule implements TestRule {
private final Context context;
public MockInitialContextRule(Context context) {
this.context = context;
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, TestContextFactory.class.getName());
TestContextFactory.setCurrentContext(context);
try {
base.evaluate();
} finally {
System.clearProperty(Context.INITIAL_CONTEXT_FACTORY);
TestContextFactory.clearCurrentContext();
}
}
};
}
}
Then in the test I inject the context and initialize the rule:
@Inject
private Context context;
@Rule
public MockInitialContextRule mockInitialContextRule = new MockInitialContextRule(context);
@Test
public void testFoo()
{
Object foo = context.lookup("jdbc/NavDS");
assertNotNull(foo);
}
Since I already posted a bounty, I'm going to leave this open for now. If someone has a better solution, or an improvement on what I ended up with, please feel free to leave an answer.