JUnitでJMockitを使ってみる

JUnitでJMockitを使ってみる

簡単なクラスを作成してJMockitの使い方をマスターします。

以下、mainクラスを持つクラスです。

package com.confrage;

public class SampleMain{
  public static void main(String[] args) {
    System.out.println(getAge());
  }

  public static int getAge() {
    return 10;
  }
}

普通に実行すれば、以下のように表示されます。

10

このクラスに対して以下テストクラスを作成します。

import mockit.Mock;
import mockit.MockUp;

import org.junit.Test;

import com.confrage.SampleMain;

public class SampleMainTest{
  @Test
  public void test001() {
    new MockUp<SampleMain>() {
      @Mock(invocations = 1)
      public int getAge() {
        return 20;
      }
    };
    SampleMain.main(new String[0]);
  }
}

getAgeで10を返すのですが、テストクラスでJMockitを使用しメソッドをハックします。

戻り値を20に変えています。

これでテストを実行します。結果は以下のようになります。

20

JUnitでJMockitを使ってみる

このテストに使ったライブラリは以下です。

junit-4.12.jar

jmockit-1.8.jar

プロシージャの戻り値をモックする

次にJavaからプロシージャを呼び出し、その戻り値をモックしてみます。

CallableStatementをモックする場合はDBUnitを使用しないとモックできないようです。

以下、Javaからプロシージャを呼び出す例です。

package com.confrage;

import java.sql.Array;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Types;

public class TestProc{
  public static void main(String[] args) {
    Connection conn = null;
    String path = "jdbc:oracle:thin:@localhost:1521:XE";
    String id = "USER002";
    String pw = "USER002";

    String sql = "CALL PACK.proc001(?,?,?,?,?,?)";

    try {
      // JDBCドライバをロード
      Class.forName("oracle.jdbc.driver.OracleDriver");

      // DBへのコネクションを作成
      conn = DriverManager.getConnection(path, id, pw);
      conn.setAutoCommit(false);

      // 実行するプロシージャを指定してパラメータをセットする
      try (CallableStatement cs = conn.prepareCall(sql)) {
        // OUTパラメータ
        cs.registerOutParameter(4, Types.INTEGER);
        cs.registerOutParameter(5, Types.VARCHAR);
        cs.registerOutParameter(6, Types.ARRAY, "INFOARRAY");

        // INパラメータ
        cs.setInt(1, 100);
        cs.setString(2, "2");
        cs.setString(3, "3");

        // プロシージャを実行する
        cs.executeUpdate();

        // 実行結果取得
        int ret = cs.getInt(4);
        String retMsg = cs.getString(5);
        Array arr = cs.getArray(6);

        Object[] val = (Object[])arr.getArray();
        for(int i = 0;i<val.length;i++) {
          java.sql.Struct struct = (java.sql.Struct)val[i];
          Object[] attr = struct.getAttributes();
          String str1 = attr[0].toString();
          String str2 = attr[1].toString();
          System.out.println(str1);
          System.out.println(str2);
        }
      }

    } catch (Exception ex){
      ex.printStackTrace();
    } finally {
      try {
        conn.close();
      } catch (SQLException e){
        throw new RuntimeException();
      }
    }
  }
}

このJavaでプロシージャの戻り値をcs.getInt()で取得していますが、これをモックします。

参考:JMockitでプロシージャ戻り値のモック

import java.sql.CallableStatement;
import java.sql.SQLException;
import mockit.Mock;
import mockit.MockUp;
import org.junit.Test;
import com.confrage.TestProc;

public class TestProcTest{
  @Test
  public <C extends CallableStatement> void test001() {
    new MockUp<C>() {
      @Mock(invocations = 1)
      int getInt(int i) throws SQLException {
        return -1;
      }
    };
    TestProc.main(new String[0]);
  }
}

これでモックにより-1を返すようにします。が、実際はモックできません。かなり悩みましたがどうやらCallableStatementのようなojdbcのクラスはDBUnitが必要なようです。(ググっても見つかりませんでしたが、、多分そのはず、、だれか詳しい方教えてください)

ということでDBUnitのライブラリをクラスパスに含めます。

dbunit-2.5.1.jar

slf4j-api-1.7.13.jar(DBUnitと依存関係にあるライブラリ)

slf4j-nop-1.7.13.jar(DBUnitと依存関係にあるライブラリ)

poi-3.14-20160307.jar(エクセルを使うため)

poi-ooxml-3.14-20160307.jar(エクセルを使うため)

xmlbeans-2.6.0.jar(DBUnitに依存?)

DBUnitを使うために抽象クラスを作成します。

まず、コネクションはDBUnit用のIDatabaseConnectionを使う必要があります。jdbcコネクションをラップします。

package com.confrage;

import java.sql.Connection;
import java.sql.DriverManager;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;

public abstract class AbstractDBUnit{

  /** DBUnit用コネクション */
  private IDatabaseConnection dbConnection = null;

  /**
  * JDBCコネクションを取得します  
  * @return connection コネクション
  * @throws Exception 例外
  */
  private Connection getJDBCConnection() throws Exception {
    String path = "jdbc:oracle:thin:@localhost:1521:XE";
    String id = "USER001";
    String pw = "USER001";
    Connection connection = DriverManager.getConnection(path, id, pw);
    connection.setAutoCommit(false);
    return connection;
  }

  /**
  * DBUnit用コネクションを取得します
  * @return IDatabaseConnection DBUnit用コネクション
  * @throws Exception 例外
  */
  protected IDatabaseConnection getConnection() throws Exception {
    if (dbConnection == null || dbConnection.getConnection().isClosed()) {
      Connection jdbcConnection = getJDBCConnection();
      dbConnection = new DatabaseConnection(jdbcConnection, jdbcConnection.getSchema());
    }
    return dbConnection;
  }
}

テストクラスはこの抽象クラスを継承します。
DBTestCaseクラスを継承せずにDBUnitを使用するには、データベーステスターを使用してみます。

package com.confrage;

import java.io.File;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;

import org.dbunit.DefaultDatabaseTester;
import org.dbunit.IDatabaseTester;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.excel.XlsDataSet;
import org.junit.After;
import org.junit.Before;

public abstract class AbstractDBUnit{

  /** DBUnit用コネクション */
  private IDatabaseConnection dbConnection = null;

  /** DBUnit用データベーステスター */
  private IDatabaseTester tester = null;

  /**
  * ベースファイルのパス
  */
  protected final String baseFile = "./src/com/confrage/baseFile.xlsx";

  /**
  * JDBCコネクションを取得します
  * @return connection コネクション
  * @throws Exception 例外
  */
  private Connection getJDBCConnection() throws Exception {
    String path = "jdbc:oracle:thin:@localhost:1521:XE";
    String id = "USER002"; 
    String pw = "USER002";
    Connection connection = DriverManager.getConnection(path, id, pw);
    connection.setAutoCommit(false);
    return connection;
  }

  /**
  * DBUnit用コネクションを取得します
  * @return IDatabaseConnection DBUnit用コネクション
  * @throws Exception 例外
  */
  protected IDatabaseConnection getConnection() throws Exception {
    if (dbConnection == null || dbConnection.getConnection().isClosed()) {
      Connection jdbcConnection = getJDBCConnection();
      dbConnection = new DatabaseConnection(jdbcConnection, jdbcConnection.getSchema());
    }
    return dbConnection;
  }

  /**
  * IDatabaseTesterを取得します
  * @return IDatabaseTester データベーステスター
  * @throws Exception 例外
  */
  protected IDatabaseTester getDatabaseTester() throws Exception {
    if (this.tester == null) {
      this.tester = new DefaultDatabaseTester(getConnection());
    }
    return this.tester;
  }

  /**
  * テストメソッド前のsetupを実行します
  */
  @Before
  public void setUp() throws Exception {
    final IDatabaseTester databaseTester = getDatabaseTester();
    databaseTester.setDataSet(getDataSet());
    databaseTester.onSetup();
  }

  /**
  * IDataSetを取得します
  *
  */
  protected IDataSet getDataSet() throws Exception {
    return new XlsDataSet(new FileInputStream(new File(baseFile)));
  }

  /**
  * テストメソッド後のtearDownを実行します
  */
  @After
  public void tearDown() throws Exception {
    if (!dbConnection.getConnection().isClosed()) {
      try {
        final IDatabaseTester databaseTester = getDatabaseTester();
        databaseTester.onTearDown();
      } finally {
        tester = null;
      }
      dbConnection.getConnection().close();
      dbConnection.close();
    }
  }
}

テストクラスは上記の抽象クラスを継承します。

import java.sql.CallableStatement;
import java.sql.SQLException;
import mockit.Mock;
import mockit.MockUp;
import org.junit.Test;
import com.confrage.TestProc;

public class TestProcTest extends AbstractDBUnit{
  @Test
  public <C extends CallableStatement> void test001() {
    new MockUp<C>() {
      @Mock(invocations = 1)
      int getInt(int i) throws SQLException {
        return -1;
      }
    };
    TestProc.main(new String[0]);
  }
}

これでtest001を実行します。

「org.dbunit.dataset.NoSuchTableException: テーブル名」とエラーが出てしまいました。これもググって調べたのですが良くわからずとりあえず存在するテーブル名をエクセルのシートに書かないといけない?ようです。(誰か知ってたら教えてください)

ということでエクセルのシート名をsample_table(当方の環境で存在するテーブルです)にしました。

JUnitでJMockitを使ってみる

再度test001を実行すると上手くデバッグもとまるし、モックできるようになりました。

これで終わり、と思ったのですが、デバッグしていると以下のロジックでgetIntのモックに移動していました。

cs.registerOutParameter(6, Types.ARRAY, "INFOARRAY");

色々調べてみるとどうも内部的にgetIntやsetStringをしているようで、そこでモックしてしまっているようです。目的は戻り値をモックしたいので、テストクラスを以下のように変更します。

import java.sql.CallableStatement;
import java.sql.SQLException;
import mockit.Mock;
import mockit.MockUp;
import org.junit.Test;
import com.confrage.TestProc;

public class TestProcTest extends AbstractDBUnit{
  @Test
  public <C extends CallableStatement> void test001() {
    new MockUp<C>() {
      int count = 0;// カウントで判断
      @Mock(invocations = 2)// 呼ばれる回数も増えているので1から2に変更
      int getInt(int i) throws SQLException {
        if (count == 1) {// カウントが1の場合のみ戻り値-1を返す
          count++;
          return -1;
        } else {
          count++;
          return 0;
        }
      }
    };
    TestProc.main(new String[0]);
  }
}

これでtest001を実行します。すると今度は「

mockit.internal.UnexpectedInvocation: Expected exactly 2 invocation(s) of TestProcTest$1#getInt(int i), but was invoked 3 time(s)」と出ました。

どうも3回呼ばれているようですがデバッグしていると以下で呼ばれていました。

Object[] val = (Object[])arr.getArray();

ということでinvocations=3とします。getIntの戻り値は0にしたら上手くいきました。

import java.sql.CallableStatement;
import java.sql.SQLException;
import mockit.Mock;
import mockit.MockUp;
import org.junit.Test;
import com.confrage.TestProc;

public class TestProcTest extends AbstractDBUnit{
  @Test
  public <C extends CallableStatement> void test001() {
    new MockUp<C>() {
      int count = 0;// カウントで判断
      @Mock(invocations = 3)// 呼ばれる回数も増えているので2から3に変更
      int getInt(int i) throws SQLException {
        if (count == 1) {// カウントが1の場合のみ戻り値-1を返す
          count++;
          return -1;
        } else {
          count++;
          return 0;
        }
      }
    };
    TestProc.main(new String[0]);
  }
}

最近はJMockitよりmockitoが主流のようです。

コメント

タイトルとURLをコピーしました