如何在 JUnit 测试中管理 JNDI 上下文?

How to Manage JNDI Context In JUnit Tests?

我目前正在为使用 guice 进行依赖注入的 java EE 项目编写一些测试。处理实体管理器及其数据源一直给我带来无穷无尽的麻烦。我尝试了多种不同的方法,但它们似乎都在某处存在致命缺陷。我似乎无法弄清楚如何在不交叉污染我的测试的情况下使用 InitialContext。

我的第一次尝试是在我的数据源的 guice 提供程序方法中手动将数据源绑定到上下文中。类似于:

    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;
    }

它有点脏,但只要我在每次测试之间重置数据源,它就可以用于单元测试。问题是,我还需要使用嵌入式码头服务器进行集成测试。集成测试有自己的 guice 模块,但如果我调用 new InitialContext() 它最终会给我在之前的测试中创建的实例。这意味着我无法创建测试套件,因为我的单元测试在集成测试之前已经污染了 Context 运行.

我的下一次尝试是尝试使用工厂来模拟上下文查找,如下所述:How to fake InitialContext with default constructor。所以像:

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;
    }

}

问题是,其他代码(如实体管理器)也在对我的模拟上下文进行调用。我最终不得不模拟 InitialContext 中的每个方法以避免异常。

我还尝试在我的提供者方法中不使用上下文。我实际上并没有在我的测试中进行任何手动查找,所以我认为我应该能够初始化和 return 数据源。那也是一个失败。实体管理器期望绑定数据源。如果我不将我的数据源绑定到上下文中,实体管理器将在初始化期间抛出异常。

在这一点上我没有想法。如果有人有更好的方法来处理这个问题(除了简单地嘲笑一切),我很想听听。谢谢

在研究这个问题的另一个想法的过程中,我终于在这里找到了部分解决方案:Junit Testing JNDI InitialContext outside the application server

关键见解是能够 'reset' 测试之间的上下文。一旦我完成了这项工作,我就能够在测试之间清理我的上下文。这样做消除了模拟 Context 的需要;我可以使用真实的实现,并且对它在每次测试开始时所处的状态充满信心。

测试模块的最终版本如下所示:

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;
}

上下文工厂更改为:

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();
    }

}

请注意,由于我使用的是不同的 jndi 实现(jetty 而不是 tomcat),我必须修改上下文设置以匹配。

完成后,我只需要测试中的代码来实际初始化和重置上下文。我按照示例使用了 JUnit 规则,但您可以使用 @Before 和 @After 方法完成同样的事情。规则最终看起来像这样:

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();
                }
            }
        };
    }

}

然后在测试中我注入上下文并初始化规则:

@Inject
private Context context;

@Rule
public MockInitialContextRule mockInitialContextRule = new MockInitialContextRule(context);

@Test
public void testFoo()
{
    Object foo = context.lookup("jdbc/NavDS");
    assertNotNull(foo);
}

由于我已经发布了赏金,所以我暂时将其打开。如果有人有更好的解决方案,或者对我最终的结果进行改进,请随时留下答案。