1. 概述

在开发中,如果应用依赖 JNDI 获取数据源,单元测试时我们通常不希望连接真实数据库。✅
取而代之的是,使用一个 mock 的数据源,让测试更轻量、隔离性更强,避免环境依赖导致测试不稳定。

本文将展示如何在 Spring 环境下,通过 Spring 自带工具 和第三方库 Simple-JNDI 两种方式,测试一个 mock 的 JNDI 数据源。

⚠️ 注意:本文只关注单元测试。关于如何在 Spring 应用中使用 JNDI + JPA 构建正式项目,可参考我们另一篇文章:Spring 集成 JPA 与 JNDI 数据源


2. JNDI 快速回顾

简单来说,JNDI(Java Naming and Directory Interface)的作用是:

将逻辑名称绑定到外部资源(如数据库连接、消息队列等),让应用通过名称查找资源,而无需关心具体实现。

核心概念:

  • 所有操作都基于一个 上下文(Context)
  • 起始点是 InitialContext,它是整个 JNDI 查找的入口
  • 通过 bind("name", object) 绑定资源
  • 通过 lookup("name") 查找资源

❌ 没有 InitialContext,JNDI 就无法工作。

举个例子,应用代码可能这样获取数据源:

DataSource ds = (DataSource) new InitialContext().lookup("java:comp/env/jdbc/datasource");

我们的目标就是在测试中,让这个 lookup 调用能返回一个 mock 的 DataSource,而不是去连真实数据库。


3. 使用 Spring 的 SimpleNamingContextBuilder 测试 JNDI 数据源

Spring 提供了一个非常方便的工具类:SimpleNamingContextBuilder,可以快速搭建一个内存中的 JNDI 环境,专用于测试。

✅ 优点:无需额外依赖,Spring 自带
⚠️ 注意:该类自 Spring 5.2 起已标记为废弃,推荐使用 Simple-JNDI 替代

步骤一:初始化 JNDI 上下文

我们在每个测试前构建一个空的命名上下文:

@BeforeEach
public void init() throws Exception {
    SimpleNamingContextBuilder.emptyActivatedContextBuilder();
    this.initContext = new InitialContext();
}

emptyActivatedContextBuilder() 会创建或复用一个全局的上下文构建器,比直接 new 更灵活。

步骤二:绑定并查找数据源

接下来写测试,验证能否成功 bind 和 lookup 一个 DataSource

@Test
public void whenMockJndiDataSource_thenReturnJndiDataSource() throws Exception {
    this.initContext.bind("java:comp/env/jdbc/datasource", 
      new DriverManagerDataSource("jdbc:h2:mem:testdb"));
    DataSource ds = (DataSource) this.initContext.lookup("java:comp/env/jdbc/datasource");

    assertNotNull(ds.getConnection());
}

关键点:

  • 使用 bind()DriverManagerDataSource 绑定到标准 JNDI 名
  • 使用 lookup() 按名查找,类型转换后验证连接可用
  • 如果 lookup 名称不存在,会直接抛异常 ❌

💡 小贴士:DriverManagerDataSource 来自 Spring JDBC,适合测试场景,生产环境请用 HikariCP 等连接池。


4. 使用 Simple-JNDI 测试 JNDI 数据源(推荐方式)

由于 SimpleNamingContextBuilder 已废弃,更推荐使用功能更强、配置更灵活的 Simple-JNDI 库。

✅ 优势

  • 支持从属性文件加载数据源配置
  • 兼容非 Java EE 环境(如 Spring Boot)
  • 可模拟复杂的 JNDI 层级结构

步骤一:引入依赖

pom.xml 中添加:

<dependency>
    <groupId>com.github.h-thurow</groupId>
    <artifactId>simple-jndi</artifactId>
    <version>0.23.0</version>
    <scope>test</scope>
</dependency>

最新版可在 Maven Central 查询。

步骤二:配置 jndi.properties

src/main/resources 下创建 jndi.properties 文件:

java.naming.factory.initial=org.osjava.sj.SimpleContextFactory
org.osjava.sj.jndi.shared=true
org.osjava.sj.delimiter=.
jndi.syntax.separator=/
org.osjava.sj.space=java:/comp/env
org.osjava.sj.root=src/main/resources/jndi

关键配置说明:

配置项 说明
java.naming.factory.initial 指定 JNDI 上下文工厂
org.osjava.sj.jndi.shared 所有 InitialContext 共享内存 ✅
org.osjava.sj.space 设置 JNDI 查找的根路径
org.osjava.sj.root 指定属性文件存放目录

⚠️ 注意路径分隔符和命名空间设置,避免 ENC(Environment Naming Context)查找失败。

步骤三:定义数据源配置文件

src/main/resources/jndi 目录下创建 datasource.properties

ds.type=javax.sql.DataSource
ds.driver=org.h2.Driver
ds.url=jdbc:h2:mem:testdb
ds.user=sa
ds.password=password

文件名和路径由 jndi.properties 中的 org.osjava.sj.root 决定。

步骤四:编写测试代码

@BeforeEach
public void setup() throws Exception {
    this.initContext = new InitialContext();
}

@Test
public void whenMockJndiDataSource_thenReturnJndiDataSource() throws Exception {
    String dsString = "org.h2.Driver::::jdbc:h2:mem:testdb::::sa";
    Context envContext = (Context) this.initContext.lookup("java:/comp/env");
    DataSource ds = (DataSource) envContext.lookup("datasource/ds");

    assertEquals(dsString, ds.toString());
}

说明:

  • lookup("java:/comp/env") 进入配置的命名空间
  • lookup("datasource/ds") 对应 jndi/datasource.properties 中的 ds 对象
  • Simple-JNDI 会自动根据 .type 创建对应实例

💡 踩坑提醒:路径拼写、斜杠方向、文件位置一定要对,否则 lookup 会抛 NameNotFoundException


5. 总结

本文介绍了两种在 Spring 中测试 JNDI 数据源的方法:

方式 是否推荐 适用场景
SimpleNamingContextBuilder ❌ 不推荐(已废弃) 老项目兼容
Simple-JNDI ✅ 强烈推荐 所有新项目

📌 核心要点:

  • 单元测试中应避免依赖真实数据库 ✅
  • 使用 mock 数据源提升测试速度和稳定性
  • 推荐使用 Simple-JNDI + 属性文件 的方式,配置清晰、易于维护
  • 注意 JNDI 路径、命名空间和文件位置的匹配

所有示例代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-persistence-simple


原始标题:Test a Mock JNDI Datasource with Spring