BDDツール spock の Mock

Groovy の BDDツール spock における Mock の使い方を簡単にご紹介します。spock の Mock は定義が簡単なので個人的にはかなり有用だと考えています。


例えば、以下のような記述でモックの処理内容が定義できます。(実行回数と戻り値の組み合わせも可)
戻り値の箇所ではクロージャを使って例外の発生などを行う事も可能です。

モックの定義例
モックオブジェクト名.メソッド名(引数の制約, ・・・) >> 戻り値
モックオブジェクト名.メソッド名(引数の制約, ・・・) >>> [戻り値1回目, 戻り値2回目, ・・・]
実行回数 * モックオブジェクト名.メソッド名(引数の制約, ・・・)

なお、引数の制約では以下のような記述が可能です。

引数の制約例
モックオブジェクト名.メソッド名()             //引数なし
モックオブジェクト名.メソッド名(_)            //何でもよい
モックオブジェクト名.メソッド名(!null)        //null以外
モックオブジェクト名.メソッド名(値)           //値と等しい
モックオブジェクト名.メソッド名(!値)          //値と等しくない
モックオブジェクト名.メソッド名(_ as クラス)  //指定クラスのインスタンス
モックオブジェクト名.メソッド名({・・・})     //クロージャで制約を実装

詳細は http://code.google.com/p/spock/wiki/Interactions をご覧ください。


それでは、実際に spock で Mock を使ったサンプルを作成していきます。(ソースは http://github.com/fits/try_samples/tree/master/blog/20110814/

今回は、以下のようなレガシーな感じの Java コードのスペック(テスト)を作成する事にします。(最近は @Inject とかのアノテーションを使って dao 部分を DI するのが普通だと思います)

テスト対象 Java コード
package fits.sample.service;

import fits.sample.model.Task;
import fits.sample.dao.TaskDao;
import fits.sample.dao.DaoFactory;

public class ToDoService {
    private TaskDao dao = DaoFactory.getInstance().getTaskDao();

    public String getTaskTitle(Integer taskId) throws NoTaskException {
        Task t = dao.getTask(taskId);
        if (t == null) {
            throw new NoTaskException();
        }
        return t.getTitle();
    }

    public boolean addTask(String title) {
        boolean result = false;
        try {
            dao.addTask(title);
            result = true;
        } catch (Exception ex) {
        }
        return result;
    }
}

スペック(テスト)の作成

上記の Java コードに対するスペックは以下のようになります。

  • Mock() もしくは Mock(インターフェース名) でモックを作成
  • Groovy の ExpandoMetaClass を使って dao フィールドの値をモックに置き換え
  • ">> {例外オブジェクト}" を使ってモックから例外を throw

なお、spock では when: で実行する処理を、then: で検証内容を実装できます。(setup: とかもあります)

スペック(ToDoServiceSpec.groovy)
package fits.sample.service

import spock.lang.*

import fits.sample.dao.*
import fits.sample.model.*

class ToDoServiceSpec extends Specification {
    def service
    TaskDao mockDao

    def setup() {
        service = new ToDoService()

        //モックの定義
        //def mockDao と定義して mockDao = Mock(TaskDao) でも可
        mockDao = Mock()

        //dao フィールドの値をモック mockDao に置き換え
        ToDoService.metaClass.setAttribute(service, "dao", mockDao)
    }

    def "タスクの追加に成功すると true"() {
        /*
        //setup() を使わずフィーチャー毎にセットアップ内容を記載する事も可
        setup:
            def service = new ToDoService()
            def mockDao = Mock(TaskDao)
            ・・・
        */
        when:
            def res = service.addTask("test")

        then:
            mockDao.addTask("test") >> new Task(taskId: 1, title: "test")
            res == true
    }

    def "タスクの追加に失敗すると false"() {
        when:
            def res = service.addTask(null)

        then:
            //モックで例外を throw
            mockDao.addTask(null) >> {throw new IllegalArgumentException()}
            res == false
    }


    def "登録済みタスクのタイトル取得"() {
        when:
            String res = service.getTaskTitle(1)

        then:
            mockDao.getTask(1) >> new Task(taskId: 1, title: "test")

            res == "test"
    }

    def "未登録タスクのタイトル取得は例外発生"() {
        when:
            String res = service.getTaskTitle(2)

        then:
            mockDao.getTask(2) >> null

            //クロージャを使って引数の制約を記載する事も可
            //mockDao.getTask({it == 2}) >> null
            //引数の値がどうでも良ければ以下でも可
            //mockDao.getTask(_) >> null

            //例外の発生を検証
            thrown(NoTaskException)
    }
}

ちなみに、フィールドの値を変更する MetaClass.setAttribute メソッドの引数は以下のように指定します。

setAttribute の使用方法
クラス名.metaClass.setAttribute(インスタンス, フィールド名, 新しい値)

スペックの実行

今回は Maven を使って実行する事にします。

id:fits:20110321 での pom.xml と基本的に同じ内容ですが、Groovy 1.8.5 用に更新しました。

pom.xml
<project ・・・>
  ・・・
  <build>
    ・・・
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <includes>
            <include>**/*Spec.java</include>
          </includes>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.codehaus.gmaven</groupId>
        <artifactId>gmaven-plugin</artifactId>
        <version>1.4</version>
        <configuration>
          <providerSelection>1.8</providerSelection>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>1.8.5</version>
      <scope>test</scope>
    </dependency>
    <!-- spock の設定 -->
    <dependency>
      <groupId>org.spockframework</groupId>
      <artifactId>spock-core</artifactId>
      <version>0.5-groovy-1.8</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
実行例
> mvn test
・・・
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running fits.sample.service.ToDoServiceSpec
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.483 sec

Results :

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------