Cucumber(キューカンバー)は、自然言語に近い文で書いたシナリオをそのまま自動テストとして実行するツールです。仕様とテストを1つのファイルで管理でき、BDD(振る舞い駆動開発)の実行基盤として使われます。Java向けのCucumber-JVMは2026年9月24日に5年ぶりのメジャー版8.0.0を公開し、Java 17が必須になり、JUnit 4用のRunnerは削除予定になりました。この記事では、Cucumber-JVM 8.0.4でmvn testを、Cucumber.js 13.3.0とPlaywrightでnpx cucumber-jsを実際に通した設定とコードを使い、Cucumberテストの書き方と実行手順、8.0で変わった点を説明します。
まとめ:Cucumberテストの要点
- Cucumberとは:Gherkin記法のfeatureファイルを読み、各ステップに対応するコード(ステップ定義)を呼び出して実行するテストツール。ライセンスはMIT。
- 最新版:Cucumber-JVM 8.0.4(2026-10-02)。Java 17以上が必要で、JUnit 4用の
cucumber-junitとTestNG用のcucumber-testngは削除予定の非推奨。 - Javaでの実行:
cucumber-java・cucumber-junit-platform-engine・junit-platform-suiteを入れ、@Suiteクラスを置いてmvn testで動かす。 - 8.0で壊れやすい点:html・json・messageのレポートにJacksonの依存追加が必要になった。無いとテスト全体がエラーで止まる。
- 実行対象の絞り込み:タグ式を
-Dcucumber.filter.tagsに渡す。@Suiteのアノテーションに書いた設定はコマンドラインから上書きできない。READMEにある行番号指定のコマンドは、シナリオが失敗してもMavenのビルドが成功扱いになった。 - JavaScriptの場合:
@cucumber/cucumber(13.3.0)とPlaywrightを組み合わせ、ブラウザはWorldとフックで管理する。ステップ定義をアロー関数で書くとthisが使えない。
Cucumberの仕組みと対応言語
Cucumberのテストは、featureファイル・ステップ定義・実行設定の3つでできています。featureファイルにはGiven(前提)・When(操作)・Then(結果)でシナリオを書き、Cucumberが1行ずつ対応するステップ定義を探して実行します。キーワードの一覧や日本語キーワードなど、Gherkin記法そのものはGherkinとは?構文の全キーワードと.featureファイルの書き方で扱っています。
照合で見落とされやすいのは、ステップ定義を探すときにキーワードが無視される点です。公式のGherkinリファレンスは「Keywords are not taken into account when looking for a step definition.」と明記しています。Given 商品を登録するとWhen 商品を登録するは同じステップとして扱われ、別々の処理を割り当てることはできません。
「cucumber」は英語でキュウリを意味し、テストツールとしてのCucumberはSmartBear社が2019年6月に買収したCucumber Ltd.が開発元です。オープンソース版と別に、SmartBearは有料のCucumberStudio(受け入れテストの設計・実行を共同で行うクラウド製品)を提供しています。公式が「official」に分類している主な実装と、2026年10月16日時点の最新版は次のとおりです。
| 実装 | 言語 | 最新版 | 配布元 |
|---|---|---|---|
| Cucumber-JVM | Java・Kotlin | 8.0.4 | Maven Central(io.cucumber) |
| Cucumber-JS | JavaScript・TypeScript | 13.3.0 | npm(@cucumber/cucumber) |
| Cucumber-Ruby | Ruby | 12.0.0 | RubyGems(cucumber) |
Cucumber-RubyのCHANGELOGによると、2026年10月12日の12.0.0でRuby 3.1と3.2のサポートを終え(必要なRubyは3.3以上)、strictの考え方を廃止しました。未定義・保留中のステップを含むシナリオは、リトライで成功したflakyなもの以外すべて失敗として扱われます。公式の一覧にはScala版とC++版(Cucumber.cpp)もあります。Pythonには公式実装が無く、Gherkinを読める別プロジェクトのbehave(1.3.3)やpytest-bdd(9.0.0)を使います。
Cucumber 8.0の変更点:Java 17必須とJUnit 4 Runnerの非推奨
Cucumber-JVM v8.0.0のリリースノートは冒頭で「It has been about five years since the last major version.」と述べ、変更の中心をJava 17への移行としています。7系から上げるときに影響が出るのは次の項目です。
| 項目 | 7系 | 8.0以降 |
|---|---|---|
| 必要なJava | Java 8以上 | Java 17以上 |
| JUnit 4 Runner | 利用可 | 削除予定の非推奨 |
| TestNG連携 | 利用可 | 削除予定の非推奨 |
| Jackson | Cucumberに同梱 | 利用者が依存を追加 |
| ステップ定義の可視性 | public必須 | package-privateも可 |
| JUnit Platform | 1系(JUnit 5) | 6.1系(JUnit 6) |
JUnit 4用のio.cucumber.junit.Cucumberクラスには@Deprecated(forRemoval = true)が付きました。@RunWith(Cucumber.class)と@CucumberOptionsで書かれた既存のRunnerは8.0でもまだ動きますが、次の段階で消える前提で、JUnit Platform Suiteへ移すのが公式の方針です。リリースノートによると、Maven Surefire 3.6.0がJUnit Platformだけで動くようになり、JUnit 4やTestNG経由で実行する必要がなくなったことが非推奨化の理由です。
Java EE向けのcucumber-openejbとcucumber-cdi2は削除され、Jakarta EE版のcucumber-jakarta-openejb・cucumber-jakarta-cdiへ置き換わりました。cucumber-deltaspikeは後継なしで削除されています。
8.0.0の後は、8.0.1(ステップ引数の位置ずれ)、8.0.2(AndroidでMethod::canAccessが使えない問題)、8.0.3(各フォーマッターの依存更新)、8.0.4(glueクラスの絞り込み条件の適用漏れ)と修正版が続き、2026年10月2日の8.0.4が最新です。8.0系を使うなら、8.0.0や8.0.1で止めずに最新の修正版へ上げてください。
JavaでCucumberテストを動かす手順(Maven+JUnit Platform)
ここからのコードは、2026年10月16日にTemurin 25.0.4.1・Maven 3.9.11・Cucumber-JVM 8.0.4・JUnit 6.1.3・Maven Surefire 3.6.0の組み合わせでmvn testを実行し、成功を確認したものです。題材はカートの合計金額を計算するクラスで、ファイル構成は次のとおりです。
src
├── main/java/com/example/cart/Cart.java
└── test
├── java/com/example/cart
│ ├── CartSteps.java … ステップ定義
│ ├── Hooks.java … フック
│ └── RunCucumberTest.java … 実行用のSuiteクラス
└── resources
├── junit-platform.properties
└── com/example/cart/cart.feature
pom.xmlの依存関係:cucumber-bomとjunit-bom
公式のスターター(cucumber-java-skeleton)と同じく、2つのBOMで版をそろえ、個々の依存には版を書きません。AssertJはステップ定義の検証に使います。最後のJacksonは、後の章で扱うhtmlレポートを使う場合だけ必要です。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-bom</artifactId>
<version>8.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit-platform-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
<!-- html・json・message プラグインを使うときに必要(8.0から) -->
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.2.3</version>
<scope>test</scope>
</dependency>
</dependencies>
ビルド設定ではmaven.compiler.releaseを17以上にし、maven-surefire-plugin 3.6.0を指定します。naming-strategy=longは、Scenario Outlineの各行をSurefireの結果で区別するための公式スターターの設定です。
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<properties>
<configurationParameters>
cucumber.junit-platform.naming-strategy=long
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
Gradleを使う場合、公式リリースノートはGradle 9.4.0以降ならfeatureファイルをJUnit Platformから直接見つけられると説明しています。
featureファイル:Background・Scenario Outline・データテーブル
テスト対象のクラスは、商品の単価と数量から合計金額を計算するだけのものです。
package com.example.cart;
import java.util.LinkedHashMap;
import java.util.Map;
public class Cart {
private final Map<String, Integer> prices = new LinkedHashMap<>();
private final Map<String, Integer> items = new LinkedHashMap<>();
public void registerPrice(String name, int price) { prices.put(name, price); }
public void add(String name, int qty) {
if (!prices.containsKey(name)) throw new IllegalArgumentException("未登録の商品: " + name);
items.merge(name, qty, Integer::sum);
}
public int total() {
return items.entrySet().stream().mapToInt(e -> prices.get(e.getKey()) * e.getValue()).sum();
}
}
featureファイルはsrc/test/resourcesの下に、テストコードと同じパッケージ階層で置きます。Backgroundは各シナリオの前に毎回実行され、Scenario OutlineはExamplesの行ごとに1回ずつ実行されます。この例ではシナリオが3回動きます。
Feature: カートの合計金額
購入者として、カートに入れた商品の合計金額を確認したい
Background:
Given 次の商品が登録されている
| name | price |
| りんご | 120 |
| みかん | 80 |
@smoke
Scenario: 1種類の商品を入れる
When "りんご" を 3 個カートに入れる
Then 合計金額は 360 円になる
@regression
Scenario Outline: 複数の商品を入れる
When "りんご" を <apple> 個カートに入れる
And "みかん" を <orange> 個カートに入れる
Then 合計金額は <total> 円になる
Examples:
| apple | orange | total |
| 1 | 1 | 200 |
| 2 | 5 | 640 |
ステップ定義:Cucumber Expressionsの{string}と{int}
ステップ定義はアノテーションの文字列でステップと結び付けます。{int}は整数、{string}は引用符で囲んだ文字列、{word}は空白を含まない1語、{float}は小数に一致し、引数の型へ自動で変換されます。正規表現も使えますが、1つの式の中でCucumber Expressionsと混ぜることはできません。
package com.example.cart;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class CartSteps {
private final Cart cart = new Cart();
@Given("次の商品が登録されている")
void registerPrices(List<Map<String, String>> rows) {
rows.forEach(r -> cart.registerPrice(r.get("name"), Integer.parseInt(r.get("price"))));
}
@When("{string} を {int} 個カートに入れる")
void addToCart(String name, int qty) {
cart.add(name, qty);
}
@Then("合計金額は {int} 円になる")
void totalIs(int expected) {
assertThat(cart.total()).isEqualTo(expected);
}
}
クラスとメソッドにpublicが無い点が8.0の変更です。7系ではpublicが必須でしたが、8.0ではpackage-privateとprotectedも使えます。ただしDIにcucumber-picocontainerを使う場合だけは、クラスとコンストラクタをpublicにする必要があります。データテーブルはList<Map<String, String>>で受け取ると、1行目の見出しをキーにした行のリストになります。
Cucumberはシナリオごとにステップ定義クラスのインスタンスを作り直します。フィールドのcartはシナリオをまたいで残らないため、前のシナリオの状態が次のシナリオに漏れることはありません。
実行用のSuiteクラスとmvn testの結果
Maven SurefireはfeatureファイルのようなJavaクラス以外のテストを自分で見つけられません(cucumber-junit-platform-engineのREADMEが、この制約とSurefireのissue #2065を挙げています)。そのため、JUnit Platform Suiteのクラスを1つ置き、そこからCucumberを起動します。
package com.example.cart;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
@Suite
@IncludeEngines("cucumber")
@SelectPackages("com.example.cart")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.cart")
public class RunCucumberTest {
}
レポートの出力先などの設定は、src/test/resources/junit-platform.propertiesに書きます。cucumber.publish.quiet=trueは、実行後に出るCucumber Reports(結果の公開サービス)の案内表示を消す設定です。
cucumber.plugin=pretty, html:target/cucumber.html
cucumber.publish.quiet=true
$ mvn test
@smoke
Scenario: 1種類の商品を入れる # classpath:com/example/cart/cart.feature:11
✔ Given 次の商品が登録されている # com.example.cart.CartSteps.registerPrices(java.util.List)
✔ When "りんご" を 3 個カートに入れる # com.example.cart.CartSteps.addToCart(java.lang.String,int)
✔ Then 合計金額は 360 円になる # com.example.cart.CartSteps.totalIs(int)
…
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
ステップ定義が見つからない行があると、Cucumberはそのシナリオをエラーにして、貼り付けて使えるメソッドの雛形を出力します。featureファイルを先に書き、失敗するテストから実装を始める進め方(TDDとは?テスト駆動開発の基本サイクルと導入判断を実装者向けに解説で説明しているサイクルと同じ順序)では、この出力をそのまま使えます。
io.cucumber.junit.platform.engine.UndefinedStepException:
The step 'クーポン "SALE10" を適用する' is undefined.
You can implement this step using the snippet(s) below:
@When("クーポン {string} を適用する")
public void クーポン_を適用する(String string) {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java.PendingException();
}
…
[ERROR] Tests run: 4, Failures: 0, Errors: 1, Skipped: 3
[INFO] BUILD FAILURE
未定義のステップは成功扱いにならず、Mavenのビルドも失敗します。featureファイルだけを先にリポジトリへ入れる運用では、次の章のタグ(例:@wip)で実行対象から外しておきます。
ステップ定義クラス間で状態を共有する:cucumber-picocontainer
ステップが増えてステップ定義を複数のクラスに分けると、クラスごとに別のインスタンスが作られるため、フィールドに持ったCartは共有されません。公式ドキュメントの「Sharing state」は、ステップ定義の間で状態を共有するにはDIモジュールを使うよう勧めており、アプリケーションが他のDIを使っていなければPicoContainerを推奨しています。依存を1つ足します(版はcucumber-bomがそろえます)。
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<scope>test</scope>
</dependency>
共有したいオブジェクトをコンストラクタの引数で受け取ります。PicoContainerは引数の型を見てCartを1つ作り、同じシナリオ内のCartStepsとCouponStepsの両方に同じインスタンスを渡します。Cartには割引率を受け取るapplyDiscount(int percent)と割引率のフィールドを追加し、既存のtotal()を次の実装に置き換えます。
private int discountPercent = 0;
public void applyDiscount(int percent) {
discountPercent = percent;
}
public int total() {
int sum = items.entrySet().stream()
.mapToInt(e -> prices.get(e.getKey()) * e.getValue())
.sum();
return sum * (100 - discountPercent) / 100;
}
この例では、割引後の金額の小数部分を切り捨てます。
public class CartSteps {
private final Cart cart;
public CartSteps(Cart cart) {
this.cart = cart;
}
// @Given・@When・@Then のメソッドは前と同じ
}
public class CouponSteps {
private final Cart cart;
public CouponSteps(Cart cart) {
this.cart = cart;
}
@When("{int}% 引きのクーポンを適用する")
void applyCoupon(int percent) {
cart.applyDiscount(percent);
}
}
@regression
Scenario: クーポンで割り引く
When "りんご" を 5 個カートに入れる
And 10% 引きのクーポンを適用する
Then 合計金額は 540 円になる
このシナリオを足して実行すると、And 10% 引きのクーポンを適用するはCouponSteps.applyCoupon、前後のステップはCartStepsで処理され、「Tests run: 4, Failures: 0」で成功しました。合計が前のシナリオの金額に積み上がらないことから、Cartはシナリオごとに作り直されているとわかります。
picocontainerを使うときに失敗した3つの書き方と、そのときの結果は次のとおりです。
- picocontainerを入れずにコンストラクタへ引数を書く:全シナリオが「does not have an single accessible zero-argument constructor」でエラーになる。
- ステップ定義クラスをpackage-privateにする:glueの読み込みで「is not instantiable by Pico Container」が出て、シナリオが1件も実行されない(Tests run: 1, Errors: 1)。メッセージは「the class is public」「the class is not abstract」「if nested, the class is static.」の3条件を挙げる。
- クラスはpublicでコンストラクタだけpackage-private:そのクラスのステップを使うシナリオだけが
PicoCompositionExceptionでエラーになり、他のシナリオは成功する。
この条件はフックのクラスにも及びます。前の手順のHooksをpackage-privateのままpicocontainerを足すと、「Glue class com.example.cart.Hooks is not instantiable by Pico Container」でシナリオが1件も動きませんでした。8.0でpackage-privateにできるのはメソッドと、DIを使わないときのクラスです。picocontainerを使うなら、ステップ定義とフックのクラス、コンストラクタはpublicにします。Spring Bootのアプリケーションでは、picocontainerの代わりに後の章のcucumber-springを使います。
フックの実行順:@Before・@Afterとorder属性
フックはシナリオやステップの前後に処理を差し込む仕組みです。@Before・@Afterはシナリオごと、@BeforeStep・@AfterStepはステップごと、@BeforeAll・@AfterAllは全シナリオの前後に1回だけ動きます。@BeforeAllと@AfterAllは引数なしのstaticメソッドにする決まりです。
フックはどのクラスに書いても全シナリオに適用され、公式READMEは「The order in which hooks are executed is not defined.」としています。順序が必要ならorder属性で指定します。
package com.example.cart;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.Scenario;
public class Hooks {
@Before(order = 1)
void openBrowser() { System.out.println("before order=1"); }
@Before(order = 20)
void loginAsTestUser() { System.out.println("before order=20"); }
@After(order = 20)
void logout() { System.out.println("after order=20"); }
@After(order = 1)
void closeBrowser(Scenario scenario) {
System.out.println("after order=1 " + scenario.getStatus());
}
}
before order=1
before order=20
after order=20
after order=1 PASSED
@Beforeはorderの小さい順、@Afterは大きい順に動きました。ブラウザ起動を先にして終了を最後にしたい場合、どちらも同じ小さい値(ここでは1)を付ければ後処理が前処理の逆順になります。orderを省略したときの値は10000です。@Before("not @api")のようにタグ式を渡すと、特定のシナリオでだけフックを動かせます。
タグで実行するシナリオを絞る方法
シナリオやFeatureの上に@smokeのようなタグを付けると、タグ式で実行対象を選べます。タグ式はand・or・notと括弧を組み合わせられます。
# @smoke だけを実行
mvn test -Dcucumber.filter.tags="@smoke"
# @regression のうち @wip を除く
mvn test -Dcucumber.filter.tags="@regression and not @wip"
# 括弧でまとめる
mvn test -Dcucumber.filter.tags="(@smoke or @regression) and not @slow"
冒頭のfeatureファイルに-Dcucumber.filter.tags="@smoke"を付けて実行すると、結果は「Tests run: 3, Skipped: 2」になり、@regressionの2件は実行されずスキップとして数えられました。
@ConfigurationParameterと-Dの設定優先順位
実行時の設定を@Suiteクラスの@ConfigurationParameterに書くかjunit-platform.propertiesに書くかで、コマンドラインから変えられるかどうかが変わります。検証では、@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")を書いたまま-Dcucumber.plugin=html:target/cucumber.htmlを付けても、HTMLファイルは作られませんでした。同じ設定をjunit-platform.propertiesへ移すと、-Dcucumber.plugin=json:target/c2.jsonがpropertiesの内容を上書きし、JSONだけが出力されました。
JUnitのユーザーガイド(Configuration Parameters)は、Launcherに直接渡された設定がシステムプロパティより優先され、システムプロパティはjunit-platform.propertiesより優先されると説明しています。CIでタグやレポート形式を切り替える予定があるなら、@ConfigurationParameterにはglueのような固定の値だけを書き、それ以外はjunit-platform.propertiesに置いてください。
単一シナリオの実行:Surefire 3.6.0での行番号指定の制約
cucumber-junit-platform-engineのREADMEは、featureファイルの10行目のシナリオだけを動かす方法としてmvn test -Dsurefire.includeJUnit5Engines=cucumber -Dcucumber.plugin=pretty -Dcucumber.features=path/to/example.feature:10を示しています。この記事の例でcart.feature:11を指定すると、1シナリオだけが実行されました。
ただし、期待値を999円に変えてわざと失敗させても、コンソールに✘ Then 合計金額は 999 円になると出るだけで、Surefireの集計は「Tests run: 0, Failures: 0」、結果はBUILD SUCCESSでした。Surefireのnaming-strategyの設定を外しても同じです。手元で1件だけ確かめるならこれで足りますが、CIでこの形のコマンドを使うと、失敗したシナリオがあってもジョブは成功になります。
CIで特定のシナリオを選ぶなら、Suiteクラスを通したままcucumber.filter.nameかタグで絞ります。8.0でcucumber.filter.nameは部分一致になったため、mvn test -Dcucumber.filter.name="1種類"で同じシナリオを選べます。この形では同じ失敗が「Tests run: 3, Failures: 1, Skipped: 2」と集計され、BUILD FAILUREになりました。
レポート出力:HTML・JSON・JUnit XMLとJacksonの追加
レポートはcucumber.pluginに「プラグイン名:出力先」をカンマ区切りで並べて指定します。
| プラグイン | 出力 | Jackson |
|---|---|---|
| pretty | コンソールへの整形表示 | 不要 |
| html | 単一HTMLファイル | 必要 |
| json | JSONレポート | 必要 |
| junit | JUnit XML | 不要 |
| message | Cucumber Messages(NDJSON) | 必要 |
| rerun | 失敗シナリオの一覧 | 不要 |
8.0でJacksonがCucumberに同梱されなくなり、html・json・message・timeline・usage-jsonの各プラグインは利用者側でJacksonを追加する必要があります。追加しないままhtml:target/cucumber.htmlを指定すると、シナリオは1件も実行されず、次のエラーでテスト全体が失敗しました(出力先には0バイトのファイルだけが残ります)。
io.cucumber.core.exception.CucumberException: Cucumber needs a JSON library to write reports.
Please ensure one of following dependencies is available.
Jackson 2, which requires
- com.fasterxml.jackson.databind.json.JsonMapper from com.fasterxml.jackson.core:jackson-databind
- com.fasterxml.jackson.datatype.jdk8.Jdk8Module from com.fasterxml.jackson.datatype:jackson-datatype-jdk8
Jackson 3, which requires
- tools.jackson.databind.json.JsonMapper from tools.jackson.core:jackson-databind
Jackson 2ならjackson-databindとjackson-datatype-jdk8の2つ、Jackson 3ならtools.jackson.core:jackson-databindの1つを入れれば解消します。Jackson 3.2.3を追加した状態では、約1MBのcucumber.htmlが生成されました。8.0へ上げた後のログに「Cucumber needs a JSON library to write reports.」が出ていれば、原因はこの依存の不足です。
CIでは、JenkinsやGitHub ActionsのテストレポートはJUnit XMLを読むのが一般的です。junit:target/cucumber.xmlを指定すればJacksonなしで出力できます。受け入れテストをビルドとデプロイの流れに組み込む設計から相談したい場合は、DevOps・CI/CD導入支援も参考にしてください。
Spring BootでCucumberを使う設定
Spring Bootのアプリケーションコンテキストをステップ定義から使うには、cucumber-springを依存に加え、glueのパッケージに設定クラスを1つだけ置きます。
package com.example.app;
import io.cucumber.spring.CucumberContextConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
@CucumberContextConfiguration
@SpringBootTest
public class CucumberSpringConfiguration {
}
@CucumberContextConfigurationを付けるクラスは1つに限ります。ステップ定義クラスはSpringが生成するため、@AutowiredでBeanを注入できます。ただしcucumber-springのREADMEによると、@MockitoBeanのようにテスト実行リスナーに依存する機能が効くのは、この設定クラスだけです。ステップ定義のBeanはシナリオごとに作り直され、シナリオ間で状態を共有しません。Spring Boot 4.1系の構成はSpring Bootとは|4.1系の依存名の変更と最小Web APIの作り方を参照してください。
JavaScript(Cucumber.js)とPlaywrightによるブラウザテストの実行手順
JavaScript・TypeScriptでは@cucumber/cucumberを使い、ブラウザ操作はPlaywrightのライブラリを組み合わせるのが一般的な構成です。ここでは2026年10月16日に、Node.js 26.5.0・@cucumber/cucumber 13.3.0・playwright 1.64.1で次のコードを実行しました。13.0.0でNode.js 26に対応しており、対応するNode.jsは22・24・26以上です。
npm i -D @cucumber/[email protected] [email protected]
npx playwright install chromium
設定ファイルを置かなければ、featuresディレクトリの下のfeatureファイルと、同じ階層のステップ定義・サポートファイル(.js)が読み込まれます。構成は次の3ファイルです。
features
├── search.feature
├── step_definitions/search.steps.js
└── support/world.js
WorldとフックによるPlaywrightのブラウザ管理
Cucumber.jsはシナリオごとにWorldと呼ぶオブジェクトを新しく作り、ステップ定義のthisに渡します。シナリオごとに新しいBrowserContextとページを作り、その参照をWorldに持たせることで、Cookieや画面の状態を分離します。ブラウザ本体の起動は時間がかかるので、BeforeAllで1回だけ行います。
Feature: 商品検索
購入者として、商品名で検索して在庫を確かめたい
Scenario: 名前で絞り込む
Given 商品一覧ページを開いている
When "りんご" で検索する
Then 検索結果に 1 件表示される
Scenario: 該当なし
Given 商品一覧ページを開いている
When "ぶどう" で検索する
Then 検索結果に 0 件表示される
const { setWorldConstructor, BeforeAll, AfterAll, Before, After, World } = require('@cucumber/cucumber');
const { chromium } = require('playwright');
// ブラウザは全シナリオで1つを共有し、コンテキストとページはシナリオごとに作り直す
let browser;
class CustomWorld extends World {
async openProductList() {
// 検証用に、検索欄と結果リストだけのページを直接読み込む
await this.page.setContent(`
<input id="q"><button id="go">検索</button>
<ul id="list"></ul>
<script>
const items = ['りんご', 'みかん', 'バナナ'];
document.getElementById('go').onclick = () => {
const q = document.getElementById('q').value;
document.getElementById('list').innerHTML =
items.filter(i => i.includes(q)).map(i => '<li>' + i + '</li>').join('');
};
</script>`);
}
}
setWorldConstructor(CustomWorld);
BeforeAll(async () => { browser = await chromium.launch(); });
AfterAll(async () => { await browser?.close(); });
Before(async function () {
this.context = await browser.newContext();
this.page = await this.context.newPage();
});
After(async function () {
await this.context?.close();
});
const { Given, When, Then } = require('@cucumber/cucumber');
const assert = require('node:assert/strict');
Given('商品一覧ページを開いている', async function () {
await this.openProductList();
});
When('{string} で検索する', async function (keyword) {
await this.page.fill('#q', keyword);
await this.page.click('#go');
});
Then('検索結果に {int} 件表示される', async function (count) {
assert.equal(await this.page.locator('#list li').count(), count);
});
$ npx cucumber-js
............
2 hooks (2 passed)
2 scenarios (2 passed)
10 steps (10 passed)
「10 steps」には、3ステップ×2シナリオのほか、シナリオごとに動いたBeforeとAfterのフックが含まれます。「2 hooks」はBeforeAllとAfterAllです。HTMLレポートは--format html:report.htmlで出力でき、このとき約950KBのファイルができました。Java版と違い、JSON用の依存を追加する必要はありません。
アロー関数とタイムアウトの落とし穴
ステップ定義をasync () => {}のアロー関数に書き換えると、thisがWorldを指さなくなり、「TypeError: this.openProductList is not a function」で両方のシナリオが失敗しました。cucumber-jsのWorldの説明のとおり、v10.8.0以降はパッケージからworldをインポートすればアロー関数からもWorldを使えます。world.openProductList()に書き換えると成功しました。thisを使うならfunctionで書く、アロー関数を使うならworldを使う、のどちらかにそろえます。
もう1つの落とし穴はタイムアウトです。公式のタイムアウトの説明によると、非同期のフックとステップは既定で5000ミリ秒でタイムアウトします。ページ遷移や外部APIを待つE2Eテストでは足りないことがあるため、setDefaultTimeout(60 * 1000)で全体を延ばすか、Before({ timeout: 60 * 1000 }, ...)のように個別に指定します。
Playwright Testのテストランナー(並列実行・トレース・レポート)をそのまま使いたい場合は、featureファイルをPlaywright Testのテストに変換するplaywright-bdd(2026年10月時点で9.2.1・@playwright/test 1.44以上が必要)も選択肢になります。Gherkinを読めるE2Eフレームワークとしては、CodeceptJSとは?E2Eテスト自動化の特徴とPlaywright統合・導入手順で扱っているCodeceptJSもあります。
Cucumberを採用するかの判断:JUnitとの使い分け
JUnitで書くテストとCucumberで書くテストの違い
Javaのプロジェクトでは、CucumberはJUnitの代わりではなく、JUnit Platformの上で動く別のテストエンジンです。同じmvn testで両方を動かせるので、どちらか一方を選ぶのではなく、テストの層ごとに分けます。
| 観点 | JUnit | Cucumber |
|---|---|---|
| テストの書き方 | Javaのテストメソッド | featureファイル+ステップ定義 |
| 主な読み手 | 開発者 | 開発者・QA・業務担当者 |
| 向いている層 | 単体・結合 | 受け入れ・E2E |
| テスト1件の追加 | メソッドを1つ書く | シナリオを書き、未定義のステップを実装 |
| 実行の仕組み | Jupiterエンジン | cucumberエンジン |
Cucumberの公式ドキュメントは、BDDの日々の活動をDiscovery(具体例を話し合って合意する)・Formulation(自動化できる形で例を書く)・Automation(例をテストとして動かしながら実装する)の3つの実践で説明し、「If you’re new to BDD, discovery is the right place to start.」と書いています。Cucumberが受け持つのは3つ目のAutomationで、1つ目の話し合いが無いまま導入すると、featureファイルは開発者だけが読むテストコードの言い換えになります。
Cucumberを採用しないほうがよい場面
Cucumberの利点は、エンジニア以外がfeatureファイルを読み、仕様の確認に参加できることにあります。この前提が成り立たないチームでは、Cucumberは自然文とコードの二重管理を増やすだけです。次のどれかに当てはまるなら、JUnitやPlaywright Testでテストを直接書くほうが保守は軽くなります。
- featureファイルを読むのも書くのもエンジニアだけで、企画・QA・顧客がレビューに入らない。
- ステップの文言が「ボタンをクリックする」「URLを開く」のような画面操作の手順になっていて、業務上の振る舞いを表していない。この場合、テストコードを日本語に書き直しただけになる。
- 単体テストの代わりに使おうとしている。Cucumberは受け入れテストの層に向いており、計算ロジックの境界値はJUnitとは?Javaの単体テストフレームワークの基本と書き方【2026年版】で書けば、ステップ定義を介さずに入力と期待値をテストメソッドへ直接書ける。
逆に、受け入れ条件を業務担当者と一緒に例で詰める進め方をしているなら、featureファイルがそのまま合意の記録になります。BDDの進め方と導入の判断基準はBDDとは?振る舞い駆動開発の仕組みとTDDとの違い・導入判断を実装者向けに解説で詳しく説明しています。E2Eテストの比率をどこまで上げるかは、テストピラミッドとは?単体・結合・E2Eの配分と実装者向けテスト戦略設計の配分を目安にしてください。
よくある質問
Cucumberテストとは何ですか?
Gherkin記法で書いたシナリオを、Cucumberがステップ定義のコードと結び付けて自動実行するテストです。多くは受け入れテストやE2Eテストの層で使い、ブラウザ操作はSeleniumとは?WebDriverの仕組みと4系の実行手順・ツール選定を解説やPlaywright(プレイライト)とは?仕組み・できること・Seleniumとの違いを解説を組み合わせます。
CucumberとGherkinの違いは何ですか?
Gherkinはシナリオを書くための記法で、Cucumberはその記法で書かれたファイルを読んで実行するツールです。GherkinのパーサーはCucumberのプロジェクトが管理していますが、behaveやpytest-bddなどCucumber以外のツールもGherkinを読めます。
Cucumber 8はJava 11で使えますか?
使えません。Cucumber-JVM 8.0.0で必要なJavaは17以上になりました。Java 11のままの場合は7系を使います。7系の最新は2026年9月22日公開の7.34.9です。
@RunWith(Cucumber.class)のRunnerは使えなくなりましたか?
8.0でもまだ動きますが、JUnit 4用のcucumber-junitは削除予定の非推奨です。@Suiteと@IncludeEngines("cucumber")を使うJUnit Platform Suiteへ移行してください。@CucumberOptionsのglueはcucumber.glue、pluginはcucumber.pluginというキーに書き換えて、@ConfigurationParameterかjunit-platform.propertiesへ移します。
特定のシナリオだけを実行するには?
タグを付けて-Dcucumber.filter.tags="@smoke"を指定するか、-Dcucumber.filter.name="シナリオ名の一部"で名前を指定します。READMEにあるfeatureファイルの行番号で指定する方法でも1シナリオだけ動きますが、Maven Surefire 3.6.0での実測では、失敗してもビルドが成功扱いになりました。CIではタグか名前で絞ってください。