◆ Problem
You want to test against a database, but after each test the database is in a slightly different state.
◆ Background
JUnit practitioners often say that “shared test setup smells.” This is our cute way of saying that the desire to share fixture data between tests is an indication of a design problem; if not now, then soon (but not for the rest of your life—you can always refactor). By “shared fixture” we mean the case when test #1 updates the fixture by adding some data, and then test #2 sees the data that test #1 has added.
This breaks test isolation and, by now, our opinion on that point should be clear.
If not, then please keep reading.
Instead of sharing fixture data, code your tests so that they share a common starting point, and then add or remove data as needed for the individual test.
Sometimes that common starting point is a clean slate, and sometimes it is a known set of data. It is possible to organize your fixture so that it is easy to extract into a separate class and reuse in many tests. This recipe describes how to do that.
◆ Recipe
First things first: extract any tests you write against a live database to its own fix- ture. See recipe 3.4, “Factor out a test fixture,” for details on how to do that. Once you have a fixture for your database tests, the general strategy is this.
347 Manage external data in your test fixture
1 In setUp(), connect to the database and prime it with whatever data you need.
2 In tearDown(), delete all data from the database.
If you would like to do something a little tricky, you can start a transaction in setUp(), and then roll it back in tearDown() so that JDBC never actually commits the data to the database. No data to clean up! Listing 10.12 shows an example that puts everything together.
package junit.cookbook.coffee.jdbc.test;
import java.sql.*;
public class SelectCoffeeBeansTest extends CoffeeShopDatabaseFixture { protected void setUp() throws Exception {
super.setUp();
Connection connection = getConnection();
connection.setAutoCommit(false);
PreparedStatement insertStatement = connection.prepareStatement(
"insert into catalog.beans "
+ "(productId, coffeeName, unitPrice) values "
+ "(?, ?, ?)");
registerStatement(insertStatement);
insertCoffee(insertStatement, "001", "Sumatra", 750);
insertCoffee(insertStatement, "002", "Special Blend", 825);
insertCoffee(insertStatement, "003", "Colombiano", 810);
}
protected void tearDown() throws Exception { getConnection().rollback();
super.tearDown();
}
public void testFindExpensiveCoffee() throws Exception { Connection connection = getConnection();
PreparedStatement findExpensiveCoffeeStatement = connection.prepareStatement(
"select * from catalog.beans where unitPrice > 2000");
registerStatement(findExpensiveCoffeeStatement);
ResultSet expensiveCoffeeResults =
findExpensiveCoffeeStatement.executeQuery();
registerResultSet(expensiveCoffeeResults);
assertFalse(expensiveCoffeeResults.next());
}
Listing 10.12 Using the “rollback” trick
Need to set up superclass fixture
Enable “rollback” trick
Hide Prepared- Statement details
Don’t commit the data!
348 CHAPTER 10 Testing and JDBC
public void testFindAllCoffee() throws Exception { Connection connection = getConnection();
PreparedStatement findAllCoffeeStatement =
connection.prepareStatement("select * from catalog.beans");
registerStatement(findAllCoffeeStatement);
ResultSet allCoffeeResults =
findAllCoffeeStatement.executeQuery();
registerResultSet(allCoffeeResults);
int rowCount = 0;
while (allCoffeeResults.next()) rowCount++;
assertEquals(3, rowCount);
}
private void insertCoffee(
PreparedStatement insertStatement, String productId,
String coffeeName, int unitPrice) throws SQLException {
insertStatement.clearParameters();
insertStatement.setObject(1, productId);
insertStatement.setObject(2, coffeeName);
insertStatement.setObject(3, new Integer(unitPrice));
insertStatement.executeUpdate();
} }
If the JDBC code you test needs to commit data to the database—for example to test transactional behavior—then in tearDown() replace Connection.rollback() with the necessary JDBC code to remove the data you may have inserted, or undo any updates you may have made. This “on-the-fly undo” can become complicated very quickly, so do not go overboard. If it takes more than one minute to write the data cleanup code for a test, then try something else, because the effort is not worth it. Speaking from painful experience, once you have about 40 to 50 tests in place, some with complex “undo changes” code, the maintenance cost begins to outweigh the benefit of using the trick, and at that point you have trouble.14
Your best bet is to have a database instance that you can destroy and rebuild at any time, so that in the worst case your tearDown() code can include deleting
14A former colleague, with his distinctive accent, always liked to say, “You are going to have trouble!”
349 Manage test data in a shared database
entire tables. As one reviewer wrote, “If rebuilding the database is not easy, then make it easy.” Still, if you do not have this luxury, we will not leave you in the dark—see recipe 10.7, “Manage test data in a shared database.”
◆ Discussion
Like many of the other live database testing recipes in this chapter, this technique is most useful when you have legacy JDBC code that you cannot change or when you want to create a refactoring safety net. If you have the opportunity to replace your JDBC access code, we recommend applying the techniques we describe in the opening of this chapter. Ideally, you would not write so many tests against a live database, but rather isolate the code that needs the database, and test the rest without one. The other recipes in this chapter discuss the relevant techniques.
Be aware of IDENTITY or auto-increment columns. The more you run your tests, the higher and higher the next available row ID becomes. If you run your tests as often as we hope you will, then you might eventually run out of IDs! If you think this is a serious problem, then the simplest thing you can do is rebuild the data- base schema periodically, which resets the next available row ID. If you do not
“own the plug”15 on your test database, then you need a more sophisticated strategy.
See recipe 10.7 for suggestions.
◆ Related
■ 3.4—Factor out a test fixture