19

问题陈述

我希望能够对连接到数据库的方法运行 junit 测试。

当前设置

Eclipse Java EE IDE – Java 代码不使用任何框架。开发人员(包括我)希望在尝试将代码移动到 Spring 框架之前对当前遗留代码进行更健壮的测试,以便我们可以一路证明行为仍然正确。

JBoss 4.2 – 供应商软件的版本限制(Adobe LiveCycle ES2);我们的 Java Web 应用程序使用此 JBoss 设置来运行并使用 Adob​​e LiveCycle API。

我们一直无法在 Eclipse 中成功运行供应商配置的 JBoss——我们花了数周时间尝试这样做,包括联系为 Adob​​e LiveCycle 配置 JBoss 提供支持的公司。假设问题是 Eclipse 中设置的内存限制问题,但迄今为止,在 Eclipse 中成功启动 JBoss 服务器时更改内存设置失败。目前,让 JBoss 在 Eclipse 中运行的尝试被搁置。

数据库连接在 JBoss 启动时加载的 JNDI 数据源中定义。我们的 Web 应用程序和 Adob​​e LiveCycle 都需要创建到该数据源的连接。

代码

我在此代码片段中忽略了错误检查和类结构,以专注于问题的核心。希望这不会给其他人带来问题。方括号中的文本不是实际文本。

我们创建连接的代码是这样的:

Properties props = new Properties();
FileInputStream in = null;
in = new FileInputStream(System.getProperty("[Properties File Alias]"));
props.load(in);
String dsName = props.getProperty(“[JNDI data source name here]”); 
InitialContext jndiCntx = new InitialContext();
DataSource ds = (DataSource) jndiCntx.lookup(dsName);
Ds.getConnection();

我希望能够在不对其进行任何更改的情况下测试依赖于此代码的方法。

在 properties-service.xml 文件中引用属性文件别名:

  <!-- ==================================================================== -->
  <!-- System Properties Service                                            -->
  <!-- ==================================================================== -->

  <!-- Allows rich access to system properties.-->

<mbean code="org.jboss.varia.property.SystemPropertiesService" 
 name="jboss:type=Service,name=SystemProperties">
  <attribute name="Properties">
    [Folder Alias]=[filepath1]
    [Properties File Alias]=[filepath2]
  </attribute>
</mbean>

来自位于 filepath2 的属性文件的片段

[JNDI data source name]=java:/[JNDI data source name]

此数据源的 JNDI xml 文件设置如下:

<datasources>
  <local-tx-datasource>
    <jndi-name>[JNDI data source name here]</jndi-name>
    <connection-url>jdbc:teradata://[url]/database=[database name]</connection-url>
    <driver-class>com.teradata.jdbc.TeraDriver</driver-class>
    <user-name>[user name]</user-name>
    <password>[password]</password>
    <!-- sql to call on an existing pooled connection when it is obtained from pool -->
    <check-valid-connection-sql>SELECT 1+1</check-valid-connection-sql>
  </local-tx-datasource>
</datasources>

我对解决方案可能在哪里的想法

Is there something I can do in a @BeforeClass method in order to make the properties the above code is looking for available without JBoss? Maybe somehow using the setProperty method of the java.util.Properties class? I would also like to use the same JNDI xml file that JBoss reads from, if possible, in order to reduce duplicate configuration settings.

So far all of my research ends with the advice “Use Spring”, but I don’t think we’re ready to open that can of worms yet. I am not an expert in JBoss, but if more details of our JBoss setup are needed for a helpful answer, I will do my best to get them, though I will likely need some pointers on where to look.

Stackoverflow Research references:
Jndi lookup in junit using spring
Out of container JNDI data source
Other research references:
http://docs.oracle.com/javase/1.4.2/docs/api/java/util/Properties.html
http://docs.oracle.com/javase/jndi/tutorial/basics/prepare/initial.html

4

4 回答 4

8

There's a very simple answer to your problem, but you're not going to like it: Don't.

By definition, a unit test should verify the functionality of a single unit (the size of which may vary, but it should be self-sufficient). Creating a setup where the test depends upon web services, databases, etc. is counter-productive: It slows down your tests, it includes a gzillion of possible things that could go wrong (failed network connections, changes to data sets, ...) during the test, which have nothing to do with the actual code you are working on, and most importantly: It makes testing much, much harder and more complicated.

Instead, you should be looking for ways to decouple the legacy code from any data sources, so that you can easily substitute mock objects or similar test doubles while you are testing.

You should create tests to verify the integrity of your entire stack, but those are called integration tests, and they operate at a higher level of abstraction. I personally like to defer writing those until the units themselves are in place, tested and working - at least until you have come to a point where you no longer expect changes to service calls and protocols on a daily basis.

In your case, the most obvious strategy would be to encapsulate all calls to the web service in one or more separate classes, extract an interface that the business objects can depend on, and use mocks implementing that same interface for unit testing.

For example, if you have a business object that calls an address database, you should copy the JNDI lookup code into a new service class called AddressServiceImpl. Its public methods should mimic all the method signatures of your JNDI datasource. Those, then, you extract to the AddressService interface.

You can then write a simple integration test to verify that the new class works: Call all the methods once and see if you get proper results. The beauty of this is that you can supply a JNDI configuration that points to a test database (instead of the original one), which you can populate with test datasets to make sure you always get the the expected results. You don't necessarily need a JBoss instance for this (though I have never had any problems with the eclipse integration) - any other JNDI provider should work, as long as the data source itself behaves the same way. And to be clear: You test this once, then forget about it. At least until the actual service methods ever change.

Once you verified that the service is functional, the next task is to go through all the dependent classes and replace the direct calls to the datasource with calls to the AddressService interface. And from that point on, you have a proper setup to implement unit tests on the actual business methods, without ever having to worry about things that should be tested elsewhere ;)

EDIT

I second the recommendation for Mockito. Really good!

于 2012-10-24T16:43:49.810 回答
1

I had a very similar situation with some legacy code in JBoss AS7, for which refactoring would have been way out of scope.

I gave up on trying to get the datasource out of JBoss, because it does not support remote access to datasources, which I confirmed in trying.

Ideally though, you don't want to have your unit tests dependant on a running JBoss instance in order to run, and you really don't want them to have to run inside of JBoss. It would be counter to the concept of self-contained unit tests (even though you'll still need the database to be running :) ).

Fortunately, the initial context used by your app doesn't have to come from a running JBoss instance. After looking at this article referred to by an answer to another question, I was able to create my own initial context, populate it with my own datasource object.

This works without creating dependencies in the code because the classes under test typically run inside the container, where they simply do something like this to get the container-provided context:

InitialContext ic = new InitialContext();
DataSource ds = (DataSource)ic.lookup(DATA_SOURCE_NAME);

They don't need to specify any environment to the constructor, because it has already been set up by the container.

In order for your unit tests to stand in for the container and provide a context, you create it, and bind a name:

InitialContext ic = new InitialContext();

// Construct DataSource
OracleConnectionPoolDataSource ds = new OracleConnectionPoolDataSource();
ds.setURL("url");
ds.setUser("username");
ds.setPassword("password");

ic.bind(DATA_SOURCE_NAME, ds);

This needs to happen in each test class's @BeforeClass method.

Now the classes being tested get my initial context when running in unit tests, and the container's when deployed.

于 2014-02-04T17:38:08.627 回答
0

If you are using tools like Git and Maven this can be done easily with them. Check in a UnitTest specific properties file along side development and qa. Use Maven and its profile facilities to specify a profile that copies your UnitTest file over to where it should go, same with your dev and qa when run with different profiles active.

There is no magic to this; Spring introduces complexity more than anything. it definitly doesn't introduce simplicity like this.

于 2012-10-24T16:58:59.903 回答
0

You can run your tests with a fake InitialContext implementation, which returns whatever you need from calls to lookup(String).

A mocking/faking tool which allows such fake implementations is JMockit. The fake implementation would be written like the following:

public class FakeInitialContext extends MockUp<InitialContext>
{
    @Mock
    public Object lookup(String name)
    {
        // Return whatever is needed based on "name".
    }
}

To apply it to a JUnit/TestNG test run, add jmockit.jar to the runtime classpath (before junit.jar if this is the case) and set the "jmockit-mocks" system property to the name of the fake class: -Djmockit-mocks=com.whatever.FakeInitialContext.

Of course, you can also write true JUnit/TestNG unit tests where any dependency can be easily mocked, by using the "Expectations & Verifications" mocking API.

(PS: For full disclosure, I am the creator of the JMockit project.)

于 2012-10-24T17:19:54.677 回答