◆ Problem
You want to verify that you are correctly making domain objects from the JDBC ResultSet objects returned by your SELECT queries.
◆ Background
There are really only two things that can go wrong when executing a query with JDBC. You might issue the wrong SQL command. You might unmarshal data into objects incorrectly. The straightforward way to test this is to do it all in one go:
prime the database with data, create the SELECT statement, execute it, and then verify that you SELECTed what you expected. (That rhymes; how nice!) While this is straightforward, it does not scale very well.
318 CHAPTER 10 Testing and JDBC
After a while, every SELECT test looks the same: the differences are in the query string and whether you invoke getString(), getInt(), getBlob()or ... you get the picture. There must be a way to remove all that duplication. Surely there is, because we described how to do exactly that in the introduction to this chapter!
Once you have applied those techniques and only one part of your data access layer executes the SQL command, you are left with the responsibility of testing just those two things mentioned previously: the command and the unmarshalling logic. This recipe handles the latter; see recipe 10.2, “Verify your SQL commands,”
for details on testing the former.
◆ Recipe
We need to test turning a ResultSet into the Set of domain objects that the ResultSet represents. A direct approach is to populate a ResultSet object with known data, invoke the “make domain objects from this” method, and then com- pare the results with the expected values. Unfortunately for us, JDBC does not pro- vide a standalone implementation of ResultSet to which we can start adding data.
This is one situation in which mock objects are simple and work well. Fortunately for us, the Mock Objects project (www.mockobjects.com) provides a couple of easy-to-use ResultSet implementations: MockMultiRowResultSet for result sets with multiple rows of data and MockSingleRowResultSet for result sets with a single row of data. The latter provides a simpler interface and can perform better than the former.
NOTE The Mock Objects project—This project began as an embodiment of the ideas that Tim Mackinnon, Steve Freeman, and Philip Craig described in their paper “Endo-Testing: Unit Testing with Mock Objects.”4 It contains mock implementations of various J2SE and J2EE library classes and inter- faces. We do not rely heavily on this package—we mostly use EasyMock to implement mock objects—but the Mock Objects project does provide a useful collection of prefabricated mock objects. We recommend using their objects while you “get the feel” of mock objects (or testing objects in general) before building your own. It is important to experience the effect that mock objects have on the style of the tests that use them. As the authors themselves point out, Mock Objects (their implementation of the concept) help you focus on the interactions between objects, with- out worrying about every little implementation detail for each test.
4 www.connextra.com/aboutUs/mockobjects.pdf
319 Test making domain objects
from a ResultSet
Now that we have a result set whose data we can hard code, we can write our test for turning a ResultSet object into a domain object. We will verify that we can cre- ate a Discount object from the catalog.discount table. Listing 10.1 shows a good first test.
public void testDiscountJoinWithDiscountDefinition() throws Exception {
PercentageOffSubtotalDiscountDefintion expectedDiscountDefinition =
new PercentageOffSubtotalDiscountDefintion();
expectedDiscountDefinition.percentageOffSubtotal = 25;
Date expectedFromDate = DateUtil.makeDate(1974, 5, 4);
Date expectedToDate = DateUtil.makeDate(2000, 5, 5);
Discount expectedDiscount = new Discount(
expectedFromDate, expectedToDate,
expectedDiscountDefinition);
DiscountStoreJdbcMapper mapper = new DiscountStoreJdbcMapper();
Map rowData = new HashMap();
rowData.put(
"typeName",
PercentageOffSubtotalDiscountDefintion.class.getName());
rowData.put("fromDate", JdbcUtil.makeTimestamp(1974, 5, 4));
rowData.put("toDate", JdbcUtil.makeTimestamp(2000, 5, 5));
rowData.put("percentageOffSubtotal", new Integer(25));
rowData.put("suspended", null);
MockSingleRowResultSet resultSet = new MockSingleRowResultSet();
resultSet.addExpectedNamedValues(rowData);
assertTrue(resultSet.next());
Discount actualDiscount = mapper.makeDiscount(resultSet);
assertEquals(expectedDiscount, actualDiscount);
}
Ignoring the additional complexity of using mock objects, the test follows the usual pattern: we create the Discount object we expect, create a ResultSet with the JDBC data we want to process, and then check the mapper’s behavior. We used the MockSingleRowResultSet because our test only used a single row of data.
You can use the corresponding MockMultiRowResultSet if your test needs to oper- Listing 10.1 A test for unmarshalling ResultSet data
Store the name/value pairs for a single row
Stuff the hard coded data into a ResultSet Point the ResultSet at the first row
320 CHAPTER 10 Testing and JDBC
ate on multiple rows of data. We present the DiscountStoreJdbcMapper that passes this test in listing 10.2.5
package junit.cookbook.coffee.jdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import junit.cookbook.coffee.data.*;
import com.diasparsoftware.jdbc.JdbcMapper;
public class DiscountStoreJdbcMapper extends JdbcMapper { public Discount makeDiscount(ResultSet discountResultSet) throws SQLException {
Discount discount = new Discount();
discount.fromDate = getDate(discountResultSet, "fromDate");
discount.toDate = getDate(discountResultSet, "toDate");
discount.discountDefinition =
makeDiscountDefinition(discountResultSet);
return discount;
}
public DiscountDefinition makeDiscountDefinition(
ResultSet resultSet) throws SQLException {
String discountClassName = resultSet.getString("typeName");
if (PercentageOffSubtotalDiscountDefintion .class.getName().equals(discountClassName)) { PercentageOffSubtotalDiscountDefintion discountDefinition =
new PercentageOffSubtotalDiscountDefintion();
discountDefinition.percentageOffSubtotal = resultSet.getInt("percentageOffSubtotal");
return discountDefinition;
} else
throw new DataMakesNoSenseException(
"Bad discount definition type name: '"
+ discountClassName + "'");
} }
Listing 10.2 DiscountStoreJdbcMapper
5 The method getDate() appearing in this listing is defined in the superclass JdbcMapper. It does what you would expect: retrieves a Date value for the specified column.
Part of JdbcMapper
DataMakesNoSenseException is a new custom exception we created
TE AM FL Y
Team-Fly®
321 Test making domain objects
from a ResultSet
◆ Discussion
We ought to warn you that when using this technique it is crucial that the data in your hard-coded result set match the data you would retrieve from the production database. Be aware of trouble spots common to database programming in general (and not just JDBC), such as time zone differences, number formats and strings that are too big for a column. For example, some databases silently truncate over- sized strings, while others throw an exception to indicate that the string is too long. It is important both to understand the behavior of your live database and to identify any differences between the mock JDBC objects and your vendor’s JDBC implementation. You may need to create custom mock objects to achieve a more faithful simulation of your vendor’s JDBC provider’s idiosyncrasies.
Also, this recipe shows us that high coupling means less reuse. The JDBC ResultSet is an excellent example of an object with too many responsibilities. It does at least three important things: represents a single table row, provides an iterator over a collection of table rows, and interacts with the underlying database to fetch those rows. This is too much work for a single object. Often when we write Object Tests we want to use just one of the features such an object provides with- out its other responsibilities “getting in the way.” A class with high internal cou- pling thwarts attempts at reusing the “smaller object” that we feel is trying to get out of the “bigger object.” The task this recipe sets out to perform is a fine exam- ple of this struggle.
We only want the data; we do not want to talk to a database to get it. If JDBC were to provide a separate class representing the rows in a ResultSet then we could use the production quality implementation in place of a mock object in our tests. We would be able to test the way we map SQL data types onto Java data types and relational column names onto Value Object properties. Unfortunately there is no such separation of data from its source, forcing us to use a mock ResultSet object. We can appreciate the value of this mock object by looking at the size of the interface it implements. To write a custom mock object for the ResultSet interface requires a considerable amount of work, even if we only need a small part of that interface! The MockResultSet implementations represent the data that a live ResultSet would fetch from a database without actually doing so. If JDBC had only separated those responsibilities in the first place, we would not need to use a MockResultSet at all; and all things being equal, we prefer to test with production code, rather than simulators.
◆ Related
■ 10.2—Verify your SQL commands
322 CHAPTER 10 Testing and JDBC