如何使用 Spring 测试模拟的 JNDI 数据源?

Posted

技术标签:

【中文标题】如何使用 Spring 测试模拟的 JNDI 数据源?【英文标题】:How to test a mocked JNDI datasource with Spring? 【发布时间】:2011-08-21 21:42:52 【问题描述】:

我是 Spring 的新手,想知道如何创建使用模拟数据源的 JUnit 测试以及如何使用 JNDI 上下文?目前,我的应用程序使用来自 tomcat 的 JNDI 上下文来检索连接,并通过该连接从数据库中检索数据。所以我想我需要模拟 JNDI 调用和数据检索。关于解决这个问题的最佳方法的任何好的指示都会很棒!非常感谢!

【问题讨论】:

【参考方案1】:

您可以通过扩展 Spring 的 AbstractDataSource 创建自己的模拟数据源。

import java.sql.Connection;
import java.sql.SQLException;

import org.springframework.jdbc.datasource.AbstractDataSource;

/**
 * Mock implementation of DataSource suitable for use in testing.
 * 
 *
 */
public class MockDataSource extends AbstractDataSource 
    private Connection connection;

    /**
     * Sets the connection returned by javax.sql.DataSource#getConnection()
     * and javax.sql.DataSource#getConnection(java.lang.String, java.lang.String)
     * 
     * @param connection
     */
    public void setConnection(Connection connection) 
        this.connection = connection;
    

    /*
     * (non-Javadoc)
     * @see javax.sql.DataSource#getConnection()
     */
    public Connection getConnection()
            throws SQLException 
        return connection;
    

    /*
     * (non-Javadoc)
     * @see javax.sql.DataSource#getConnection(java.lang.String, java.lang.String)
     */
    public Connection getConnection(String username, String password)
            throws SQLException 
        return connection;
    

我会将连接的 JNDI 查找与其余代码分开。将 DataSource 注入您的数据访问对象 (DAO) 并使用 MockDataSource 来测试 DAO。

【讨论】:

如果我注入数据源,这不会消除 JNDI 查找的需要吗? 可以的。在 Spring 中有多种方法可以获取 DataSource。一旦你有了它,你可以注入。不过,Spring 可以从 JNDI 读取数据源。 我编辑了您的答案以删除第一行的缩进。现在语法高亮工作。希望你不要介意。【参考方案2】:

我通常在单独的文件中定义我的 JNDI 依赖项,例如 datasource-context.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/jee
        http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">

    <jee:jndi-lookup id="dataSource" 
        jndi-name="java:comp/env/dataSource" 
        expected-type="javax.sql.DataSource" />

</beans>

这样我就可以在测试资源中创建另一个文件并定义适合我的测试数据源,例如datasource-testcontext.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dataSource"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource"
        p:driverClassName="org.hsqldb.jdbcDriver"
        p:url="jdbc:hsqldb:hsql://localhost:9001"
        p:username="sa"
        p:password="" /> 

</beans>

然后在我的测试类中,我使用数据源的测试配置而不是依赖于 JNDI 的生产配置

@ContextConfiguration(
    "classpath*:META-INF/spring/datasource-testcontext.xml",
    "classpath*:META-INF/spring/session-factory-context.xml"
)
public class MyTest 



如果数据源未在单独的文件中定义,您仍然可以轻松地存根 JNDI 调用返回的对象:

像这样:Injecting JNDI datasources for JUnit Tests outside of a container 或使用包org.springframework.mock.jndi中的类,即。 SimpleNamingContextBuilder(在这个 calass 的 javadoc 中有一个例子)。

【讨论】:

我这样做了,但我仍然得到异常原因:javax.naming.NoInitialContextException:需要在环境或系统属性中指定类名,或作为小程序参数,或在应用程序资源中文件:java.naming.factory.initial @fastcodejava 你到底做了什么?为 JNDI 相关配置使用单独的文件?在测试设置中创建了 JNDI 上下文?还是用SimpleNamingContextBuilder 不幸的是,oracle 博客文章的链接似乎已失效。我找不到等效的替代品...【参考方案3】:

您可以使用 SimpleNamingContextBuilder 使 jndi 数据源可用于您的测试:

    SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
    builder.bind("java:comp/env/jdbc/mydatasource", dataSource);
    builder.activate();

https://fisheye.springsource.org/browse/spring-framework/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java?hb=true

这并不完全是模拟数据源,但它确实通过 jndi 使数据源可用于您的测试。

【讨论】:

我这样做了,但我仍然得到异常原因:javax.naming.NoInitialContextException:需要在环境或系统属性中指定类名,或作为小程序参数,或在应用程序资源中文件:java.naming.factory.initial【参考方案4】:

你总是可以创建一个 beans.test.xml 配置,你首先引用 beans.xml,然后覆盖数据源配置:

src/main/resources/beans.xml

<!-- Database configuration -->
<import resource="beans.datasource.jndi.xml" />

src/test/resources/beans.test.xml

<import resource="beans.xml" />
<import resource="beans.datasource.test.xml" />

JUnit 测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations =  "classpath:/beans.test.xml" )
public class ASRTests

...

在你的 jndi bean 中,声明引用

<jee:jndi-lookup expected-type="javax.sql.DataSource" id="mysqlDataSource" jndi-name="jdbc/mysql"/>

在您的测试 bean 中,声明数据源

<bean id="mysqlDataSource" ...>
...
</bean>

请记住将测试数据源 bean 移动到测试文件夹中。

【讨论】:

【参考方案5】:

Spring 的 org.springframework.jndi.JndiObjectFactoryBean 最适合 JNDI 查找。根据其文档,它还允许为基于弹簧的测试用例注入默认值。

参考下面的spring配置(命名为spring-test-db-config.xml)

<bean id="dataSource" class="oracle.jdbc.pool.OracleDataSource">
    <property name="URL" value="jdbc:oracle:thin:@localhost:1521:XE"/>
    <property name="user" value="UNITTEST"/>
    <property name="password" value="UNITTEST"/>
</bean>

<bean id="dataSourceFromJndi" class="org.springframework.jndi.JndiObjectFactoryBean">
    <!-- Any junk value will suffice as that is always gonna throw NamingException -->
    <property name="jndiName" value="jdbc/Ds"/>
    <property name="defaultObject" ref="dataSource"/>
</bean>

添加其他配置文件定义的bean参考dataSourceFromJndibean

<!-- START OF SERVICES -->
<bean class="com.sample.Service" >
    <property name="dataSource" ref="dataSourceFromJndi" />
</bean>

这种方法的优点是您可以保留 2 个不同的数据库配置文件 - 一个用于生产,另一个用于单元测试。只需导入正确的。测试配置将包含一个默认对象。

【讨论】:

【参考方案6】:

Java 配置.....

Junit 测试用例

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DatabaseConfigStub.class, loader= AnnotationConfigContextLoader.class)
public class DatabaseConfigTest  

@Autowired
private DataSource datasource;
@Autowired
private JdbcTemplate jdbcTemplate;


@Before
public void setUp() throws Exception 



@After
public void tearDown() throws Exception 


@Test
public void testDataSource() 
    assertNotNull(datasource);
    assertNotNull(jdbcTemplate);



DatabaseConfigStub

public class DatabaseConfigStub 

private static final Logger log = Logger.getLogger(DatabaseConfigStub.class);

        private static final String DS_NAME = "jdbc/DS_NAME";

@Bean
DataSource dataSource() 
    JndiObjectFactoryBean jndiObjectBean = EasyMock.createMock(JndiObjectFactoryBean.class);
    jndiObjectBean.setJndiName(DS_NAME);
    jndiObjectBean.setResourceRef(true);
    jndiObjectBean.setProxyInterfaces(DataSource.class);

    EasyMock.expect( (DataSource)jndiObjectBean.getObject()).andReturn(new DataSource() 

            public <T> T unwrap(Class<T> iface) throws SQLException 
                // TODO Auto-generated method stub
                return null;
            

            public boolean isWrapperFor(Class<?> iface) throws SQLException 
                // TODO Auto-generated method stub
                return false;
            

            public void setLoginTimeout(int seconds) throws SQLException 
                // TODO Auto-generated method stub

            

            public void setLogWriter(PrintWriter out) throws SQLException 
                // TODO Auto-generated method stub

            

            public int getLoginTimeout() throws SQLException 
                // TODO Auto-generated method stub
                return 0;
            

            public PrintWriter getLogWriter() throws SQLException 
                // TODO Auto-generated method stub
                return null;
            

            public Connection getConnection(String username, String password)
                    throws SQLException 
                // TODO Auto-generated method stub
                return null;
            

            public Connection getConnection() throws SQLException 
                // TODO Auto-generated method stub
                return null;
            
        
    );
    EasyMock.replay(jndiObjectBean);

    return (DataSource) jndiObjectBean.getObject();


@Bean
JdbcTemplate jdbcTemplate()
    return new JdbcTemplate( dataSource());

【讨论】:

【参考方案7】:

您也可以使用 Simple-JNDI。它是一个内存中的 JNDI 实现,用于在 J2EE 容器之外使用 JNDI 上下文。它允许您在生产和测试中使用相同的 bean 定义文件。假设这是您在生产中的 bean 定义:

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/jdbc/DataSource"/>
</bean>
<bean id="dao" class="my.Dao">
    <property name="dataSource" ref="dataSource" />
</bean>

像这样创建一个属性文件

type=javax.sql.DataSource
driverClassName=org.gjt.mm.mysql.Driver
url=jdbc:mysql://localhost/testdb
username=user_name
password=password

将 Simple-JNDI 和一个带有少量配置的 jndi.properties 文件放在你的类路径中。然后像往常一样访问您的数据源。

More about Simple-JNDI is found here.

【讨论】:

【参考方案8】:

我最近遇到了为我的 JUnit 测试用例模拟 JNDI DB 资源的问题。我处理创建了一个单独的 DBStub 类,其中包含模拟的 javax.sql.DataSource 并将其分配给 JNDI 命名上下文构建器 SimpleNamingContextBuilder 的 Spring 简单实现,

public class DBStub 

@Mock
DataSource dataSource;

public DBStub() 
    try 
        MockitoAnnotations.initMocks(this);
        SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.emptyActivatedContextBuilder();
        builder.bind("java:comp/env/jdbc/DataSource", dataSource);
     catch (NamingException e) 
        fail();
    

     

将此类扩展到实际的 JUnit 测试类将解决问题,

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations =  "classpath:application-context.xml" )
public class PricingOperationTest extends DBStub 

    @Autowired
    private JdbcTemplate template;

    @Test
    public void testDataSource() 
        assertNotNull(template.getDataSource());
    

【讨论】:

以上是关于如何使用 Spring 测试模拟的 JNDI 数据源?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spring Boot 中模拟数据库连接以进行测试?

将 Spring 数据源绑定到 JNDI

spring 找不到 JNDI 数据源

spring学习笔记(19)mysql读写分离后端AOP控制实例

将 jndi 数据源与 spring 批处理管理一起使用

如何使用嵌入式 Tomcat 容器在 Spring Boot 中创建 JNDI 上下文