Cucumber-JVMのHelloWorld

以前のブログでCucumber-JVMについて触れました。Cucumber-JVMは面白いプロダクトだし、使い方が非常に簡単なので、今回はCucumber-JVMのHelloWorldをブログに書きます。Cucumber-JVMの概要については以前のブログをご参照下さい。では、HelloWorldの手順を書いていきます。

Mavenでライブラリを取得

pom.xmlに依存ライブラリを追加します。タグに以下を記述すればよいです。

    <dependency>
        <groupId>info.cukes</groupId>
        <artifactId>cucumber-picocontainer</artifactId>
        <version>1.1.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>info.cukes</groupId>
        <artifactId>cucumber-junit</artifactId>
        <version>1.1.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.10</version>
        <scope>test</scope>
    </dependency>

シナリオを記述

Gherkin(ピクルス用の小さなキュウリを意味する)と呼ばれるフォーマットでシナリオを書きます。ファイル名の拡張子は、「.feature」にし、任意のパッケージに格納します。以下にシナリオのサンプルを示します。

Feature: 車の燃料の補給
  運転手は、車を走らせるために、燃料を補給する必要がある。

Scenario: 燃料の補給
 Given 車のタンクには、10リットルの燃料が入っている
 When 運転手は、50リットルの燃料を補給した
 Then タンクには、60リットルの燃料が入っている

「Senario:」がシナリオの始まりの部分です。その下にステップを記述します。

ステップの先頭のキーワードの種類には、Given、When、Then、And、Butがありますが、Cucumber-JVMにとって違いは関係ないので(ステップの順番にテストコードを実行するだけ)、使い分けはシナリオを記述する人の裁量で自由に行います。

また、1つの「.feature」ファイルに、複数のシナリオ(「Senario:」)を記述することができます。ファイルは任意のパッケージに配置します。ここでは、CarMaintenance.featureというファイル名でcom.genba.carパッケージに配置することにします。このHelloWorldでは、最終的には以下のようなファイル構成になります。


実行のためのクラスを作成

テストコードを記述クラスは後ほど紹介しますが、まずは、テストを実行するためだけのクラスを作成します。フィールドもメソッドも持たず、Annotationの@RunWithでCucumber.classを指定するだけです。このクラスを実行すると、パッケージ配下(サブパッケージ含む)のシナリオ(「.feature」ファイル)とテストコードが紐付けられてテストが実行されます(明示的に「.feature」ファイルとテストコードを指定することもできます)。以下にサンプルを示します。

package com.genba.car;

import org.junit.runner.RunWith;
import cucumber.api.junit.Cucumber;

@RunWith(Cucumber.class)
public class CarTest {}

実行のためのクラス(CarTestクラス)をJUnitで実行

まだテストコードを記述してませんが、ひとまずこの状態でCarTestクラスをJUnitで実行します。すると、コンソール画面に以下のメッセージが表示されます。

You can implement missing steps with the snippets below:

@Given("^車のタンクには、(\\d+)リットルの燃料が入っている$")
public void 車のタンクには_リットルの燃料が入っている(int arg1) throws Throwable {
    // Express the Regexp above with the code you wish you had
    throw new PendingException();
}

@When("^運転手は、(\\d+)リットルの燃料を補給した$")
public void 運転手は_リットルの燃料を補給した(int arg1) throws Throwable {
    // Express the Regexp above with the code you wish you had
    throw new PendingException();
}

@Then("^タンクには、(\\d+)リットルの燃料が入っている$")
public void タンクには_リットルの燃料が入っている(int arg1) throws Throwable {
    // Express the Regexp above with the code you wish you had
    throw new PendingException();
}

シナリオのステップに紐づかなかったテストコードの雛形が出力されています。

テストコードの雛形の作成

Cucumber-JVMでは、シナリオのステップごとにテストメソッドを作成します。先ほどコンソール画面で表示されたテストコードは、3つのテストメソッドが含まれており、それぞれ、シナリオのステップに該当しています。このテストメソッドのことをStep Definitionと呼び、Step Definitionを定義しているテストクラス群をひとまとめにしてStep Definitions(複数形のsが付く)と呼びます。まずは、この雛形をコピペしただけのテストクラスを、実行のためのクラス(CarTestクラス)のパッケージ配下(サブパッケージ含む)に作成します。ここではCarMaintenanceStepsクラスにします。以下にサンプルを示します。

package com.genba.car;

import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import cucumber.runtime.PendingException;

public class CarMaintenanceSteps {
	@Given("^車のタンクには、(\\d+)リットルの燃料が入っている$")
	public void 車のタンクには_リットルの燃料が入っている(int arg1) throws Throwable {
	    // Express the Regexp above with the code you wish you had
	    throw new PendingException();
	}

	@When("^運転手は、(\\d+)リットルの燃料を補給した$")
	public void 運転手は_リットルの燃料を補給した(int arg1) throws Throwable {
	    // Express the Regexp above with the code you wish you had
	    throw new PendingException();
	}

	@Then("^タンクには、(\\d+)リットルの燃料が入っている$")
	public void タンクには_リットルの燃料が入っている(int arg1) throws Throwable {
	    // Express the Regexp above with the code you wish you had
	    throw new PendingException();
	}
}

Annotationの中の文字列は正規表現になっています。Cucumber-JVMは、ステップとStep Definitionの紐付けを正規表現のマッチで行います。アノテーションの種類も関係ありません(例えば、ステップがWhenでStep Definitionが@Givenでも、正規表現がマッチすれば紐付けられます)。ちなみに、1つのシナリオが複数のテストクラスのStep Definitionを利用することもできるし、複数のシナリオが同じStep Definitionを利用することもできます。また、1つのステップが複数のStep Definitionにマッチしてしまうとエラーになります。「(\\d+)」のところはステップで可変で設定できる数字を表し、ステップで指定した値がテストメソッドの引数に設定されます。数字のほかにも、文字列や日付なども可変にすることができます(可変の値の種類についてはこちらを参照)。

この状態で、実行のためのクラス(CarTestクラス)を実行すると、PendingExceptionが発生し、コンソール画面で未実装のStep Definitionが出力されます。以下にメッセージを示します。

cucumber.runtime.PendingException: TODO: implement me
	at com.genba.car.CarMaintenanceSteps.車のタンクには_リットルの燃料が入っている(CarMaintenanceSteps.java:12)
	at ?.Given 車のタンクには、10リットルの燃料が入っている(com\genba\car\CarMaintenance.feature:5)

Step Definitionsの作成

Step Definitionsの中身を記述します。以下にサンプルを示します。テスト対象のCarクラスも合わせて示します。

package com.genba.car;

import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

public class CarMaintenanceSteps {
	private Car car;
	@Given("^車のタンクには、(\\d+)リットルの燃料が入っている$")
	public void 車のタンクには_リットルの燃料が入っている(int arg1) throws Throwable {
        car = new Car(arg1);
	}

	@When("^運転手は、(\\d+)リットルの燃料を補給した$")
	public void 運転手は_リットルの燃料を補給した(int arg1) throws Throwable {
        car.addFuel(arg1);
	}

	@Then("^タンクには、(\\d+)リットルの燃料が入っている$")
	public void タンクには_リットルの燃料が入っている(int arg1) throws Throwable {
        int actualFuelLevel = car.getFuelLevel();
        assertThat(actualFuelLevel, is(arg1));
	}
}
package com.genba.car;

public class Car {

    private Integer fuelLevel;

    public Car(int initialFuelLevel) {
        fuelLevel = initialFuelLevel;
    }

    public void addFuel(int addedFuel) {
        fuelLevel = fuelLevel + addedFuel;
    }

    public int getFuelLevel() {
        return fuelLevel;
    }
}

実行のためのクラス(CarTestクラス)をJUnitで実行

再度、実行のためのクラス(CarTestクラス)をJUnitで実行します。今度は、コンソール画面に何も出力されず、テストが成功します。以下は、EclipseJUnitを実行したときの結果画面です。


以上でHelloWorldは終了です。興味が沸いた方はぜひ試してみてください。