matsukaz's blog

Agile, node.js, ruby, AWS, cocos2d-xなどなどいろいろやってます

DBUnit 2.2のバグ

DBUnit 2.2を利用したテストケースを作成したら、何度やっても「java.sql.SQLException: クローズされた接続です。」ってエラーメッセージが・・・。ソースを追っかけてみてやっと原因がつかめました。
一番の問題はAbstractDatabaseTesterクラスの以下の実装。

public abstract class AbstractDatabaseTester implements IDatabaseTester{
   private void executeOperation( DatabaseOperation operation ) throws Exception
   {
      if( operation != DatabaseOperation.NONE ){
         IDatabaseConnection connection = getConnection();
         try{
            operation.execute( connection, getDataSet() );
         }
         finally{
            closeConnection( connection );
         }
      }
   }
   public void onSetup() throws Exception
   {
      executeOperation( getSetUpOperation() );
   }
   public void onTearDown() throws Exception
   {
      executeOperation( getTearDownOperation() );
   }
}

executeOperation()内でコネクションのクローズをしてるんだけど、その前のgetConnection()はコネクションを取得し直してるわけじゃなくて、AbstractDatabaseTesterクラスを実装したDefaultDatabaseTesterクラスをインスタンス化した際に受け取ったコネクションを使いまわしてるんよね。

public class DefaultDatabaseTester extends AbstractDatabaseTester {
  public DefaultDatabaseTester( final IDatabaseConnection connection ) {
    this.connection = connection;
  }
  public IDatabaseConnection getConnection() throws Exception {
    return this.connection;
  }
}

なので一度executeOperation()が呼ばれるとコネクションがクローズされちゃって、2回目はクローズされたコネクションを利用しようとして「java.sql.SQLException: クローズされた接続です。」ってエラーが出ちゃう。
で、どのタイミングでexecuteOperation()が呼ばれるかというと、DatabaseTestCaseクラスのsetUp()とtearDown()が実行されたときなんよね。

public abstract class DatabaseTestCase extends TestCase{
    protected void setUp() throws Exception
    {
        super.setUp();
        final IDatabaseTester databaseTester = getDatabaseTester();
        assertNotNull( "DatabaseTester is not set", databaseTester );
        databaseTester.setSetUpOperation( getSetUpOperation() );
        databaseTester.setDataSet( getDataSet() );
        databaseTester.onSetup();
    }
    protected void tearDown() throws Exception
    {
      try {
        final IDatabaseTester databaseTester = getDatabaseTester();
        assertNotNull( "DatabaseTester is not set", databaseTester );
        databaseTester.setTearDownOperation( getTearDownOperation() );
        databaseTester.setDataSet( getDataSet() );
        databaseTester.onTearDown();
      } finally {
        super.tearDown();
      }
    }
    protected IDatabaseTester newDatabaseTester() throws Exception{
      final IDatabaseConnection connection = getConnection();
      final IDatabaseTester tester = new DefaultDatabaseTester(connection);
      return tester;
    }
    protected IDatabaseTester getDatabaseTester() throws Exception {
      if ( this.tester == null ) {
        this.tester = newDatabaseTester();
      }
      return this.tester;
    }
}

要は、DatabaseTestCaseクラスのgetSetUpOperation()とgetTearDownOperation()をオーバーライドして、それぞれで何らかのDatabaseOperationを指定しちゃうとこの問題が発生します。

解決策としては、コネクションが使いまわされないようにするって手を考えました。毎回コネクションを取り直せばこんな問題も起こらないわけだし。実装は以下な感じ。

public abstract class FixedDatabaseTestCase extends DatabaseTestCase {
  @Override
  protected IDatabaseTester getDatabaseTester() throws Exception {
    return newDatabaseTester();
  }
}

だらだら書いたわりにそれだけかよってツッコミは勘弁してくださいw。でもこれで毎回コネクションを取り直してくれるので解決です。

上記の問題ってDBUnitのバグかなぁと思うんだけどどうなんだろ・・・?ちなみにReplacementTableで複数項目の置換ができないってバグもまだ直ってないです。DBUnitを使うときはこの辺要注意です。