Verify your tests clean up JDBC resources

Một phần của tài liệu Manning JUnit recipes practical methods for program (Trang 366 - 374)

Problem

You have written some tests that create JDBC objects such as connections, state- ments, and result sets. You want to avoid leaking resources by ensuring that your tests clean themselves up.

Background

There are a number of problems with leaking JDBC resources. For one, you can eas- ily defeat the purpose of JDBC connection pooling by holding on to connections until the database gives up waiting for you and orphans them, rendering them unusable. This article we found on the Web describes the problem in more detail.12

11Object Tests on their own are not enough to verify that you have implemented the features your end users need. That is what the End-to-End Tests are for.

12From “WebSpherePro System Admin Tips,” May 28, 2003 issue (www.e-promag.com/epnewsletters).

Use search keywords “connection pooling performance”.

336 CHAPTER 10 Testing and JDBC

NOTE Connection pool performance—Many users report that JDBC connection pool- ing enhances performance for a short time, but then actually degrades user response times significantly after a few hundred user transactions.

The problem seems to be a server performance issue, but it’s actually a coding flaw: failure to close JDBC sessions and release JDBC resources.

Opening JDBC resources is an expensive process, which is why JDBC connection pooling exists in the first place. Without pooling, if you fail to close and release JDBC connections you’re not really consuming, there is no obvious performance drain. Eventually you may run out of memory, but often that situation doesn’t occur during the life of the server.

But with connection pooling, if you fail to close JDBC sessions, you’ll eventually exhaust the pool, at which point your application will experi- ence long waits until connections get freed by other instances.

The first place to look for such problems is in the cleanup phase of an individual HTTP transaction. Even though you may have an application- level session open, you must close JDBC sessions to return them to the pool for reuse by other HTTP sessions.

If you don’t see any obvious failures to close JDBC sessions, you may be

“leaking” sessions in exception code. Any exception handler that ends the HTTP interaction needs to release JDBC resources. This is a particu- larly insidious failure mode because you may experience such exceptions infrequently, allowing a long time to lapse between server startup and application slowdown.

Although the above words were written in the context of IBM’s WebSphere Appli- cation Server, it applies to a much broader context: no matter what your platform, the more JDBC resources you leak, the more memory you leak, which impacts per- formance. You will experience frequent garbage collection and slow, silent ero- sion of the memory your application needs to do its job.

The bad news is that if you do not clean up JDBC resources, the JDBCAPI will simply let you continue not cleaning up after yourself. We imagine it sits there laughing at you while it watches your code consume every available byte of mem- ory. Fortunately, cleaning up JDBC resources in your code is not a problem. In your tests, you allocate a connection at the top of the test and close it at the bot- tom. If you allocate a statement, you close it at the bottom. What could go wrong?

The problem is that you need to clean up even if your test fails! You have no doubt coded your JDBC resource cleanup in finally statements. Those finally statements are ugly: they themselves may throw SQLExceptions all over the place.

It is not pretty. There must be a better way.

337 Verify your tests clean up JDBC resources

Recipe

You can use the following as a checklist to ensure that any test using JDBC resources cleans up after itself properly, not only in terms of cleaning up the data- base, but also in cleaning up the Java objects the test uses to talk to the database.

✔ Create the data source in setUp().

✔ Allocate connections in setUp()—you may only need one, unless you are testing transactional behavior with multiple connections.

✔ Create collections in setUp() to store any result sets, statements, and con- nections you want to clean up on your way out—one collection for each kind of resource.

✔ As you create JDBC resources in your test, add them to the “clean me up later” collections you created in the preceding step.

✔ In tearDown(), invoke close() on any of the resources you used in the test.

Be sure to test for null first, because you never know when the test ended!

After you have done this two or three times, you will notice a definite code pattern that you can likely refactor up into a Base Test Case (see recipe 3.6, “Introduce a Base Test Case”). There is also some duplication that can be pushed out to utility classes, but we’re getting ahead of ourselves. First, let’s look at an example. We have written two tests for the Coffee Shop database schema: one that verifies the existence of a catalog and table, another that verifies the existence of a uniqueness constraint on a table column. Both tests run against the database. Listing 10.7 shows the code for both tests, with some extraneous code removed.

public class CoffeeShopDatabaseSchemaTest extends TestCase { public void testTablesAndColumnsExist() throws Exception { MimerDataSource coffeeShopDataSource = new MimerDataSource();

coffeeShopDataSource.setDatabaseName("coffeeShopData");

coffeeShopDataSource.setUser("admin");

coffeeShopDataSource.setPassword("adm1n");

Connection connection = coffeeShopDataSource.getConnection();

DatabaseMetaData databaseMetaData = connection.getMetaData();

ResultSet schemasResultSet = databaseMetaData.getSchemas();

List schemaNames = new LinkedList();

while (schemasResultSet.next()) {

schemaNames.add(schemasResultSet.getString("TABLE_SCHEM"));

}

assertTrue(

Listing 10.7 Two database schema tests

338 CHAPTER 10 Testing and JDBC

CollectionUtil.stringCollectionContainsIgnoreCase(

schemaNames, "catalog"));

schemasResultSet.close();

connection.close();

}

public void testCoffeeNameUniquenessConstraint() throws Exception { MimerDataSource coffeeShopDataSource = new MimerDataSource();

coffeeShopDataSource.setDatabaseName("coffeeShopData");

coffeeShopDataSource.setUser("admin");

coffeeShopDataSource.setPassword("adm1n");

Connection connection = coffeeShopDataSource.getConnection();

connection.createStatement().executeUpdate(

"delete from catalog.beans");

PreparedStatement createBeanProductStatement = connection.prepareStatement(

"insert into catalog.beans "

+ "(productId, coffeeName, unitPrice) "

+ "values (?, ?, ?)");

createBeanProductStatement.clearParameters();

createBeanProductStatement.setString(1, "000");

createBeanProductStatement.setString(2, "Sumatra");

createBeanProductStatement.setInt(3, 725);

assertEquals(1, createBeanProductStatement.executeUpdate());

try {

createBeanProductStatement.executeUpdate();

fail("Added two coffee products with the same name?!");

}

catch (SQLException expected) {

assertEquals(String.valueOf(23000), expected.getSQLState());

} } }

The code printed in bold is duplicated in the two tests, so we extract these lines into a test fixture (see recipe 3.4, “Factor out a test fixture”).

Now that we have taken care of the connection, we need to handle the state- ments. Here is our general approach:

1 We create an instance-level collection of statements to close.

2 We identify all the places in a test where we have created a statement and, just before executing it, place it in the list of statements to close.

3 We add code in tearDown() to iterate over the list of statements to close, closing each one.

339 Verify your tests clean up JDBC resources

We repeat these three steps for result sets as well. Listing 10.8 shows the resulting code.

public class CoffeeShopDatabaseSchemaTest extends TestCase { public void testTablesAndColumnsExist() throws Exception { DatabaseMetaData databaseMetaData = connection.getMetaData();

ResultSet schemasResultSet = databaseMetaData.getSchemas();

resultSetsToClose.add(schemasResultSet);

List schemaNames = new LinkedList();

while (schemasResultSet.next()) {

schemaNames.add(schemasResultSet.getString("TABLE_SCHEM"));

}

assertTrue(

CollectionUtil.stringCollectionContainsIgnoreCase(

schemaNames, "catalog"));

}

public void testCoffeeNameUniquenessConstraint() throws Exception { Statement statement = connection.createStatement();

statementsToClose.add(statement);

statement.executeUpdate("delete from catalog.beans");

PreparedStatement createBeanProductStatement = connection.prepareStatement(

"insert into catalog.beans "

+ "(productId, coffeeName, unitPrice) "

+ "values (?, ?, ?)");

statementsToClose.add(createBeanProductStatement);

createBeanProductStatement.clearParameters();

createBeanProductStatement.setString(1, "000");

createBeanProductStatement.setString(2, "Sumatra");

createBeanProductStatement.setInt(3, 725);

assertEquals(1, createBeanProductStatement.executeUpdate());

try {

createBeanProductStatement.executeUpdate();

fail("Added two coffee products with the same name?!");

}

catch (SQLException expected) {

assertEquals(String.valueOf(23000), expected.getSQLState());

} }

private Connection connection;

private MimerDataSource dataSource;

private List statementsToClose = new LinkedList();

private List resultSetsToClose = new LinkedList();

Listing 10.8 Handling JDBC resources in the test fixture

Will be closed after each test

Will be closed after each test

Will be closed after each test

Store object to close

340 CHAPTER 10 Testing and JDBC

protected void setUp() throws Exception { dataSource = new MimerDataSource();

dataSource.setDatabaseName("coffeeShopData");

dataSource.setUser("admin");

dataSource.setPassword("adm1n");

connection = dataSource.getConnection();

}

protected void tearDown() throws Exception { for (Iterator i = statementsToClose.iterator(); i.hasNext();) { Statement each = (Statement) i.next();

each.close();

}

for (Iterator i = resultSetsToClose.iterator(); i.hasNext();) { ResultSet each = (ResultSet) i.next();

each.close();

}

if (connection != null) connection.close();

}

public MimerDataSource getDataSource() { return dataSource;

} }

Once this fixture is in place, you can take it a step further and extract the JDBC resource cleanup into its own class. Your database test fixture only needs to hold an instance to this new class—call it JdbcResourceRegistry, where you can regis- ter JDBC resources to be cleaned up. In setUp(), create a new resource registry; in tearDown(), invoke JdbcResourceRegistry.cleanup().

You can finally move the database fixture code up into a database fixture class.

This code is generally application- or component-specific, as it involves using your data source and priming it with specific fixture data. We pushed the CoffeeShop- DatabaseSchemaTest fixture objects up to a new test fixture we called CoffeeShop- DatabaseFixture, which we show in listing 10.9.

package junit.cookbook.coffee.jdbc.test;

import java.sql.*;

import junit.framework.TestCase;

import com.diasparsoftware.java.sql.JdbcResourceRegistry;

import com.mimer.jdbc.MimerDataSource;

// You should only need one database fixture for the entire project Listing 10.9 CoffeeShopDatabaseFixture

Close each

“closable” object

TE AM FL Y

Team-Fly®

341 Verify your tests clean up JDBC resources

public abstract class CoffeeShopDatabaseFixture extends TestCase { private Connection connection;

private MimerDataSource dataSource;

private JdbcResourceRegistry jdbcResourceRegistry;

protected void setUp() throws Exception { dataSource = new MimerDataSource();

dataSource.setDatabaseName("coffeeShopData");

dataSource.setUser("admin");

dataSource.setPassword("adm1n");

jdbcResourceRegistry = new JdbcResourceRegistry();

connection = dataSource.getConnection();

getJdbcResourceRegistry().registerConnection(connection);

}

protected void tearDown() throws Exception { getJdbcResourceRegistry().cleanUp();

}

public MimerDataSource getDataSource() { return dataSource;

}

protected Connection getConnection() { return connection;

}

protected JdbcResourceRegistry getJdbcResourceRegistry() { return jdbcResourceRegistry;

}

protected void registerConnection(Connection connection) { jdbcResourceRegistry.registerConnection(connection);

} protected void registerStatement(Statement statement) { jdbcResourceRegistry.registerStatement(statement);

} protected void registerResultSet(ResultSet resultSet) { jdbcResourceRegistry.registerResultSet(resultSet);

} }

Let us review how far we have come. By refactoring our database test, we have built a database fixture class from which all other database-related tests can extend. If you are testing legacy JDBC code, then this is a particularly useful design. If you plan to refactor your JDBC code in the direction of a small JDBC engine, this fixture helps you write the tests that support that refactoring effort.

All in all, a good thing. You can see the difference in listing 10.10, which shows Simple, no?

Convenience methods

342 CHAPTER 10 Testing and JDBC

package junit.cookbook.coffee.jdbc.test;

import java.sql.*;

import java.util.LinkedList;

import java.util.List;

import com.diasparsoftware.java.util.CollectionUtil;

public class CoffeeShopDatabaseSchemaTest extends CoffeeShopDatabaseFixture {

protected void tearDown() throws Exception {

Statement statement = getConnection().createStatement();

registerStatement(statement);

statement.executeUpdate("delete from catalog.beans");

super.tearDown();

}

public void testTablesAndColumnsExist() throws Exception { DatabaseMetaData databaseMetaData =

getConnection().getMetaData();

ResultSet schemasResultSet = databaseMetaData.getSchemas();

registerResultSet(schemasResultSet);

List schemaNames = new LinkedList();

while (schemasResultSet.next()) {

schemaNames.add(schemasResultSet.getString("TABLE_SCHEM"));

}

assertTrue(

CollectionUtil.stringCollectionContainsIgnoreCase(

schemaNames, "catalog"));

}

public void testCoffeeNameUniquenessConstraint() throws Exception { Statement statement = getConnection().createStatement();

registerStatement(statement);

statement.executeUpdate("delete from catalog.beans");

PreparedStatement createBeanProductStatement = getConnection().prepareStatement(

"insert into catalog.beans "

+ "(productId, coffeeName, unitPrice) "

+ "values (?, ?, ?)");

registerStatement(createBeanProductStatement);

createBeanProductStatement.clearParameters();

createBeanProductStatement.setString(1, "000");

createBeanProductStatement.setString(2, "Sumatra");

Listing 10.10 CoffeeShopDatabaseSchemaTest using the new fixture

Register objects to be closed after each test Invoke super to do the regular cleanup

Register objects to be closed after each test

Register objects to be closed after each test

343 Verify your production code

cleans up JDBC resources

createBeanProductStatement.setInt(3, 725);

assertEquals(1, createBeanProductStatement.executeUpdate());

try {

createBeanProductStatement.executeUpdate();

fail("Added two coffee products with the same name?!");

}

catch (SQLException expected) {

assertEquals(String.valueOf(23000), expected.getSQLState());

} } }

Discussion

If you need this (almost) automatic cleanup facility, then take a look at GSBase’s JDBC resource wrappers, available at gsbase.sourceforge.net. These resource wrap- pers clean up after themselves, which is quite nice of them! If you can change the JDBC code you need to test, then we recommend using these resource wrappers, even as a substitute for the techniques in this recipe.

Related

■ 3.4—Factor out a test fixture

■ 3.6—Introduce a Base Test Case

■ GSBase (http://gsbase.sourceforge.net)

Một phần của tài liệu Manning JUnit recipes practical methods for program (Trang 366 - 374)

Tải bản đầy đủ (PDF)

(753 trang)