6.5 Testing JDBC Code

6.5.1 Problem

You want to use mock objects to test JDBC code.

6.5.2 Solution

Use mock implementations of JDBC interfaces like Connection, PreparedStatement, and ResultSet.

6.5.3 Discussion

Although you could create your own implementations of the JDBC interfaces, the Mock Objects framework from http://www.mockobjects.com makes your work easier. This framework provides mock implementations of the key JDBC interfaces so you don't have to manually stub out every method of interfaces like java.sql.ResultSet. Your tests extend from and use these mock implementations.

These examples work with Version 0.4rc1 of the Mock Objects framework. You will almost certainly have to alter the code when newer versions of Mock Objects are available.

Example 6-9 shows a test for the getAccount( ) method shown in the previous recipe.

Example 6-9. Testing the getAccount( ) method
public void testGetAccount(  ) throws Exception {
    class MyMockResultSet extends MockResultSetJdk14 {

        public Object getObject(int columnIndex) throws SQLException {
            return null;
        }

        public Object getObject(String columnName) throws SQLException {
            if ("acctType".equals(columnName)) {
                return "CH";
            }
            if ("balance".equals(columnName)) {
                return new Double(100.0);
            }
            return null;
        }

        public boolean next(  ) throws SQLException {
            super.myNextCalls.inc(  );
            return true;
        }

        public int getRow(  ) throws SQLException {
            return 1;
        }
    }

    MockResultSet mockRs = new MyMockResultSet(  );
    mockRs.setExpectedCloseCalls(1);
    mockRs.setExpectedNextCalls(1);

    MockPreparedStatement mockPs = new MockPreparedStatement(  );
    mockPs.addExpectedSetParameter(1, "0001");
    mockPs.setExpectedCloseCalls(1);
    mockPs.addResultSet(mockRs);

    MockConnection mockConnection = new MockConnection(  );
    mockConnection.setupAddPreparedStatement(mockPs);
    mockConnection.setExpectedCloseCalls(0);
    AccountFactory acctFact = new AccountFactory(  );

    // call the method that we are actually testing
    Account acct = acctFact.getAccount("0001", mockConnection);

    mockRs.verify(  );
    mockPs.verify(  );
    mockConnection.verify(  );
}

MyMockResultSet is the key to this test. It extends MockResultSetJdk14 (described shortly). MyMockResultSet overrides a handful of abstract methods in order to simulate data that would normally be returned from a true database call. Our goal is to support our unit tests without relying on a real database, and we only need to stub out the actual methods that our test calls.

The remainder of the unit test should look familiar if you read through the recipes presented earlier in this chapter. Specifically, we tell the mock result set how many calls to expect. We then create and set up the mock prepared statement and connection, using them to exercise the code in AccountFactory. When finished, we ask each of the mock objects to verify themselves.

It turns out that the version of Mock Objects used in this chapter does not fully support J2SE 1.4. Specifically, many new JDBC methods are not defined in the MockResultSet class. For this reason, we created MockResultSetJdk14, as shown in Example 6-10. This class merely provides dummy implementations of the new JDBC methods so our examples compile under J2SE 1.4.

Example 6-10. Making MockResultSet work with J2SE 1.4
package com.oreilly.mock;

import com.mockobjects.sql.MockResultSet;

import java.net.URL;
import java.sql.*;

public abstract class MockResultSetJdk14 extends MockResultSet {
  public URL getURL(int columnIndex) throws SQLException {
      notImplemented(  );
      return null;
  }

  public URL getURL(String columnName) throws SQLException {
      notImplemented(  );
      return null;
  }

  public void updateRef(int columnIndex, Ref x) throws SQLException {
      notImplemented(  );
  }

  public void updateRef(String columnName, Ref x) throws SQLException {
      notImplemented(  );
  }

  // etc...
  public void updateBlob(int columnIndex, Blob x) throws SQLException
  public void updateBlob(String columnName, Blob x) throws SQLException
  public void updateClob(int columnIndex, Clob x) throws SQLException
  public void updateClob(String columnName, Clob x) throws SQLException
  public void updateArray(int columnIndex, Array x) throws SQLException
  public void updateArray(String columnName, Array x) throws SQLException
}

The fact that we had to write our own class to support J2SE 1.4 illustrates a pitfall of the mock object approach to testing. The mock objects must be kept up-to-date whenever new methods are added to the interfaces you are testing.

It is important to remember that these tests are not actually testing SQL or the database. Instead, they are testing code at the database access layer of an application by "faking out" the database.

6.5.4 See Also

The previous recipe shows how to modularize JDBC code so it is testable. The Mock Objects framework is available at http://www.mockobjects.com.