Verify your SQL commands

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

Problem

You would like to verify your SQL commands without necessarily involving the database each time.

Background

The three ingredients6 for correct JDBC code are:

1 Correct SQL commands

2 Correctly converting domain objects to PreparedStatement parameters

3 Correctly converting a ResultSet to domain objects

We have shown that the rest of the interaction with the JDBCAPI can be isolated to a single place, tested once, and then trusted forever. Verifying your SQL com- mands with JUnit is strange. When we try to write such a test, it seems to reduce to this assertion:

assertEquals(

expectedSqlString,

queryBuilder.getSqlString(statementName);

This is equivalent—isomorphic, really—to putting a key-value pair into a Map, and then verifying that you can retrieve the value with the key. It only tests the key’s hashCode() method and Java’s implementation of HashMap or TreeMap, which has nothing to do with JDBC and SQL. Is there a point to using JUnit to verify SQL commands? We do not think so.

Recipe

We recommend not writing JUnit tests to verify your SQL commands against a database. This is one case where JUnit is not the best tool for the job. Instead we recommend simply executing your SQL commands using your database’s command- line tool, such as Mimer’s BatchSQL.

Yes, you read that correctly: we recommend manual tests in this case. Have we gone mad?

6 There are more issues to handle, including connection pooling and transactions, but those are more infrastructure issues and should only be coded once in an application. Here we are referring to JDBC code you would need to write throughout an application, handling various tables, queries and updates.

323 Verify your SQL commands

No, although you may disagree with that assessment. Generally speaking, the best way to verify SQL commands is to try them out a few times, become comfort- able with them, and then treat them as “correct.” You may then write tests that ver- ify that the SQL commands still match the “last known correct version.” In short, apply the Golden Master technique.

NOTE Gold(en) Master—Also known as “golden results,” a Golden Master—or Gold Master, depending on whom you ask—is a test result that you verify once by hand, and then use as the baseline for future test runs. Future executions of that test pass if the results match the Gold Master output.

Do not confuse this technique with the well-known anti-pattern Guru Checks Output. We are talking about checking output by hand once then using that output to implement a self-verifying test. With Guru Checks Output, you need the guru to check the output of each test run, and when the guru is not around, no testing can happen. With Gold Master, we capture the guru’s knowledge once and keep it in the test for all time.

To illustrate the point, here is an example from the Coffee Shop application.

While shoppers are purchasing coffee from the online store, a product manager somewhere is updating the catalog. She needs to add products to the database when we decide to launch a new type of coffee bean. Somewhere in the system there is a line of code that creates the corresponding SQL statement to insert a new coffee bean product into the appropriate table. Let us treat this as a legacy coding scenario, meaning that the code already exists and “is correct”—at least as far as our basic observations of the system are concerned. We want to add a test to help stop us from changing the SQL statement without seeing the effects immedi- ately. In a legacy code situation this is the first line of defense.

First we locate the method that performs the SQL update:

public void addProduct(Product toAdd) { Connection connection = null;

PreparedStatement insertStatement = null;

try {

connection = dataSource.getConnection();

insertStatement =

connection.prepareStatement(

"insert into catalog.beans "

+ "(productId, coffeeName, unitPrice) values "

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

insertStatement.clearParameters();

insertStatement.setString(1, toAdd.productId);

insertStatement.setString(2, toAdd.coffeeName);

insertStatement.setInt(3, toAdd.unitPrice.inCents());

324 CHAPTER 10 Testing and JDBC

if (insertStatement.executeUpdate() != 1) throw new DataMakesNoSenseException(

"Inserted more than 1 row into catalog.beans!");

}

catch (SQLException e) {

throw new DataStoreException(e);

}

finally { try {

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

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

}

catch (SQLException ignored) { }

} }

If we are able to refactor this method, we can extract the SQL command into a method similar to the following:

public String getAddProductSqlString() { return "insert into catalog.beans "

+ "(productId, coffeeName, unitPrice) values "

+ "(?, ?, ?)";

}

Note that we generally favor extracting these strings to methods rather than to symbolic constants, because it is easier to refactor the method in something more general purpose, such as a key-based lookup method. This is just a special case of interface/implementation separation...but we digress.7

Now that we have extracted out the SQL string, we can write the following test:

public void testAddProductSqlString() throws Exception {

CatalogStoreJdbcImpl store = new CatalogStoreJdbcImpl(null);

assertEquals("", store.getAddProductSqlString());

}

Notice that we expect an empty string here, which is obviously not the string we really expect the catalog store to execute against the database. We write this test because we are unsure about the actual SQL command we are going to get, and yet the JUnit API requires that we expect something. Because we have decided to treat the actual SQL command as correct, we need to let the store tell us what that

7 It is also a small amount of Smalltalk influence, which we think is generally a Good Thing.

325 Verify your SQL commands

string is, rather than guess at it. It is much easier this way. We execute the test and receive this failure message:

expected:<> but was:<insert into catalog.beans

➾ (productId, coffeeName, unitPrice) values (?, ?, ?)>

Now we know the SQL command to expect, so we place it in the test for future ref- erence, using trusty old copy-and-paste:

public void testAddProductSqlString() throws Exception {

CatalogStoreJdbcImpl store = new CatalogStoreJdbcImpl(null);

assertEquals(

"insert into catalog.beans "

+ "(productId, coffeeName, unitPrice) values "

+ "(?, ?, ?)",

store.getAddProductSqlString());

}

This test now passes, allowing us to do some future refactoring such as generating this statement from database schema information. If our refactoring changes the SQL command in any way, this test lets us know immediately, at which point we can either update the Gold Master string or decide that something we have done has introduced a defect.

Discussion

The next step is to externalize the Gold Master string to a file, in which case a properties file suffices. Once all your SQL commands are externalized to a file you can use that as the source for your SQL queries, rather than hard coding them in your JDBC client. This refactoring adds considerable flexibility to your design. If tomorrow the database group decides to reorganize the tables using a different schema name or a different table-naming scheme, you only need to change a sin- gle file and rerun all the tests.

Now what if you are unable to refactor the SQL command as we did here? How do you determine the Gold Master string? The answer is to substitute a Data- Source implementation that collects that information for you. Rather than rein- vent the wheel, you can use Mock Objects to write essentially the same test as we have already described:

public void testAddProductSqlString() throws Exception { String expectedSqlString = "";

MockDataSource dataSource = new MockDataSource();

MockConnection2 connection = new MockConnection2();

MockPreparedStatement expectedStatement = new MockPreparedStatement();

expectedStatement.addUpdateCount(1);

Store the Gold Master string

326 CHAPTER 10 Testing and JDBC

dataSource.setupConnection(connection);

connection.setupAddPreparedStatement(

expectedSqlString, expectedStatement);

CatalogStoreJdbcImpl store =

new CatalogStoreJdbcImpl(dataSource);

Product product = new Product(

"762",

"Expensive New Coffee", Money.dollars(10, 50));

store.addProduct(product);

expectedStatement.verify();

}

When we executed this test, we received this message:

junit.framework.AssertionFailedError: com.mockobjects.sql.

CommonMockConnection2.preparedStatements does not contain insert into catalog.beans (productId, coffeeName, unitPrice) values (?, ?, ?)

This is the Gold Master string! Now we can place this in the test, assigning it to the variable expectedSqlString above. After we make that change and execute the test again, it passes! The Gold Master string is now in the test to guard against future unexpected changes. Notice that there is a bit more work to do here, as cli- ents cannot gain direct access to the SQL string, but the technique is the same:

execute the test once without knowing what to expect, let the test tell you what value to expect, and then change the test so that it expects that value from this point forward.

Remember the important distinction between Gold Master (which we like) and Guru Checks Output (which we like much less): in the former case you check the output by hand once, after which point the test becomes self-verifying; in the latter case, you have to check the output each time you execute the test. If you use Guru Checks Output, and if you are the guru, then we hope you have no plans for a vacation any time soon.

Post script

One of our reviewers, George Latkiewicz, suggested an alternative technique using JUnit. Because we have not tried this technique in a real project yet, we can- not really recommend it, but it sounds good and is worth trying.

The technique is simple. Ask the JDBC driver to compile your SQL command by creating a PreparedStatement from a live connection to the database. When you

Tell the Mock Connection which SQL string to expect

The production code will supply the SQL string

Check the Gold Master against the production code

327 Test your database schema

invoke prepareStatement(), the Connection object should throw an SQLException if the statement is incorrect. The amount of validation is not limited in this case merely to SQL syntax, but the JDBC driver should also report incorrect column and table names. You could create a Parameterized Test Case (see recipe 4.8,

“Build a data-driven test suite”) to test each SQL command your application might need to execute. The test would simply invoke prepareStatement() on each com- mand without actually executing it.8 George has found this particularly useful when the statements change frequently or when he has had to support multiple database systems or JDBC drivers (including differences among platforms). In par- ticular, George has found this technique invaluable when generating SQL com- mands dynamically.

Now we still think that the Gold Master approach, on the whole, provides bet- ter return on both the investment of effort and the investment in time executing tests, but we are only speculating. To be fair, we would need to try George’s tech- nique before drawing any conclusions. For that reason, we offer it to you as an alternative and hope that it works well for you.

Related

■ Keith Stobie, “Test Result Checking Patterns” for a description of Gold Master, also known as Batch Check, Golden Results, and Reference Checking (various web references)

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

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

(753 trang)