Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 2 of 4
Another approach might start with an empty database, and have each unit test populate it with the requisite data before executing the code to test. We would need to clean out the database between each test, so that tests could run in any order. Besides being potentially resource-intensive, this approach still suffers from the revision-control problem noted above (i.e., what if the database schema changes and becomes incompatible between releases?). Again, we are back to managing different database versions to test different software versions. Unit testing should be more seamless than this.
Fortunately, a more elegant solution exists that lets you unit test code that accesses databases, and tightly couple the code version to the database version the code expects to see, allowing both to version together in your source code repository. The solution involves, as you might expect, the subject of another mantra frequent JavaWorld readers know well: design patterns. More on those in a moment. First, we must decide on a test data form.
Ideally, our test data should be self-contained and easily version-controlled. We would need a text format compatible with most version-control systems, and a human-readable form, which would ease test data creation and debugging. Due to relational data's hierarchical nature, a flat text file is not expressive enough without some implicit assumptions. Fortunately, our good friend XML (Extensible Markup Language) fits the bill perfectly. It is text, human-readable (when formatted properly), and easily expresses hierarchical structures. If we encode the whole test data set for a particular test as a single XML file, we can construct that file with a simple text editor, and easily version-control it right alongside the code for which it provides test data.
So, how do we create an XML document that simulates a database? The following XML "database" format was created for a real-life
testing scenario, and works for most common querying scenarios. Let's suppose we want to model a database having two common
queries, which we'll label "query1" and "query2". "query1" takes one argument and returns two text columns. "query2" takes two arguments and returns one numeric column. The following XML document represents one possible database state:
<?xml version="1.0"?>
<dataSource>
<query label="query1">
<instance>
<arg>value 1</arg>
<resultSet>
<result>
<column>value 2</column>
<column>value 3</column>
</result>
</resultSet>
</instance>
<instance>
<arg>value 4</arg>
<resultSet>
<result>
<column>value 5</column>
<column>value 6</column>
</result>
<result>
<column>value 7</column>
<column>value 8</column>
</result>
</resultSet>
</instance>
</query>
<query label="query2">
<instance>
<arg>value 9</arg>
<arg>value 10</arg>
<resultSet>
<result>
<column>11</column>
</result>
</resultSet>
</instance>
</query>
</dataSource>
When queried, this particular database returns a ResultSet with one row if "query1" executes with an argument of value 1. A call to getString(1) on the ResultSet (i.e., get the value of the first column as text) returns value 2. A call to getString(2) (the second column) returns value 3. A subsequent call to resultSet.next() returns false, since only one result exists in the result set. Similarly, executing "query1" with a value 4 argument returns a ResultSet with two hits. And, executing "query2" with value 9 and value 10 arguments returns a ResultSet with one hit. A call to getInt(1) on this ResultSet returns the integer value 11.
Although this XML database's structure is fairly simple, I've included a DataSource.dtd you can use to validate your XML data source documents for particularly large data sets.
The first step toward achieving full unit-testability is to "abstract away" those system parts required to always act a certain
way, but implemented differently in different circumstances. Let's assume we wish to retrofit a running application for testing.
Here is TradeProcessor's core logic, as it runs currently:
public final void processPendingTrades()
throws Exception
{
final String query =
"select id, account, transtype, symbol, quantity,
status " +
"from trades where status=
'" + Trade.STATUS_PENDING + "'";
Connection con = null;
Statement stmt = null;
ResultSet resultSet = null;
try
{
con = DriverManager.getConnection
("jdbc:db2:tradeDB");
stmt = con.createStatement();
resultSet = stmt.executeQuery(query);
while (resultSet.next())
{
final String id = resultSet.getString(1);
final String account = resultSet.getString(2);
final String transType = resultSet.getString(3);
final String symbol = resultSet.getString(4);
final int quantity = resultSet.getInt(5);
final String status = resultSet.getString(6);
final Trade trade =
new Trade(id, account,
transType, symbol, quantity, status);
process(trade);
}
}
finally
{
if (resultSet != null)
resultSet.close();
if (stmt != null)
stmt.close();
if (con != null)
{
con.rollback();
con.close();
}
}
}
This fairly standard code executes a query and steps through the result set, processing each row. When we run the application
for real, the TradeProcessor must retrieve a list of currently pending trades from the database. In test mode, the TradeProcessor should retrieve that list from an XML file constructed specifically to test a particular situation.
An elegant, object-oriented approach to this problem inserts an abstraction layer between the processing logic and the implementation.
In other words, the processing logic continues to act on an object that behaves like a ResultSet, but which may or may not actually talk to a real Java Database Connectivity (JDBC) database. The object represents abstractly
a "ResultSet-like" object. The point is that the processing logic only cares that the object it talks to responds to commands such as
next() and getString(). This approach of creating an interface for which the implementation may vary is so common in object-oriented programming
that it has earned the distinction of being identified as the Bridge design pattern.
The solution then creates a new set of abstract types that capture the abstract nature of querying a data source and retrieving
results. In addition to the ResultSet type, we must also create a DataSource type (which represents either the database or the XML file), and a Query type (which, when passed to the DataSource, allows the DataSource to create a ResultSet). These abstract types are packaged under com.paulitech.query in the example code for this article. The figure below shows a UML representation.