- 技術 💻
Cucumberとは?使い方を日本語Gherkinの実行結果で解説【BDD】
結論
Cucumber(キューカンバー)は、Gherkin という決まった形の文章(「前提・もし・ならば」)で書いた仕様を、そのまま自動テストとして実行するツールです。文章の1行ずつを、開発者が書いた「ステップ定義」のコードに結びつけて動かします。BDD(振る舞い駆動開発)を進めるために作られました。
この記事でわかること
- 日本語の feature ファイルとステップ定義を書いて動かす手順と、実際の出力
- 未定義・あいまい・構文エラーなど、つまずきやすいエラーの原文と直し方
- Playwright と組み合わせた受け入れテストと、単体テストとの使い分け
Cucumber は、テストを「人が読める仕様書」と「動くテストコード」の2つに分けて書きます。仕様書の側は企画・業務の担当者も読み書きでき、コードの側は開発者が書きます。
flowchart TD
F[featureファイル<br>日本語の仕様] --> C[Cucumber<br>1行ずつ読む]
C --> Q{一致する定義は?}
Q -->|1個| S[ステップ定義]
Q -->|0個・複数| E[undefined か<br>ambiguous]
S --> APP[テスト対象<br>関数・画面]
APP --> R[passed / failed]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| Cucumberとは | Gherkin で書いた仕様を自動テストとして実行するBDDのツール | Cucumberとは |
| Gherkinとは | Feature・Scenario・Given/When/Then で書く仕様の文法。Cucumber はそれを動かす道具 | Gherkinの書き方 |
| 日本語で書ける? | 書ける。1行目に # language: ja。「機能・シナリオ・前提・もし・ならば」 |
Gherkinの書き方 |
| Cucumber 使い方(最初の1本) | npm で入れ、features に .feature とステップ定義を置き npx cucumber-js |
最初のテストを動かす |
| 同じシナリオを値だけ変えて回す | シナリオアウトラインと「例」の表 | シナリオアウトライン |
| ブラウザを操作したい | Playwright をフックで起動し、ステップ定義から操作する | Playwrightと組み合わせる |
| Undefined と出た | ステップ定義が無い。出力された雛形を貼って中身を書く | よくあるエラー |
| Java で使いたい | Cucumber-JVM。8.0 から Java 17 以上と JUnit Platform | Java(Cucumber-JVM) |
| 使うべき? | 業務の人と仕様を合わせたい受け入れテストに向く。計算の細かい確認は単体テスト | 判断表 |
確認環境:Ubuntu 24.04.5 LTS の上で、Node.js 22.23.4 と npm 10.9.9 を使い、作業用のフォルダーに @cucumber/cucumber 13.3.0(同梱の @cucumber/gherkin 42.0.1・@cucumber/cucumber-expressions 20.1.0)と playwright-core 1.64.1 を入れて実行しました。ブラウザは Chromium 153.0.8010.12(Chrome for Testing・ヘッドレス)で、操作したのは 127.0.0.1 で起動した自作の画面だけです。比べるための単体テストは Node.js 標準の node:test で書いています。Cucumber のレポートを外部に公開する機能(--publish)は使っていません。Java 版(Cucumber-JVM)は、この環境から Maven Central への接続が許可されなかったため動かしておらず、Cucumber 公式ドキュメント・cucumber-jvm リポジトリの CHANGELOG と README で書いています。マシンは 2 CPU で、ほかの作業と同時に動いているため、時間の数値は目安です。図は mermaid 11.16.0 で描画を確かめました(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Cucumberとは?使い方を日本語Gherkinの実行結果で解説【BDD】」
Cucumberとは(BDDのテストツール)
この節では、Cucumber が何をする道具で、どんな部品からできているかが分かります。
Cucumber は、「こう振る舞うべき」という具体例を文章で書き、その文章を自動テストとして実行するツールです。 公式サイトは、Cucumber を BDD(Behaviour-Driven Development、振る舞い駆動開発)を支えるために作られたツールと説明しています。もともとは Ruby で書かれ、今は JavaScript・Java など複数の言語に実装があります。
Cucumber で使う部品は3つです。
1feature ファイル
拡張子 .feature。Gherkin の文法で「機能」と「シナリオ」を書く。業務の担当者も読める仕様書になる。
2ステップ定義
「単価 {int} 円の…」のような文の型と、それが来たときに動かすコード。JavaScript なら .js、Java なら .java。
3実行の設定
どの feature を、どのステップ定義で、どの形式のレポートで動かすか。cucumber.cjs や JUnit の Suite クラスに書く。
Cucumber は文の「前提・もし・ならば」の部分は見ず、その後ろの文がステップ定義の型と一致するかだけで結びつけます。そのため、同じステップ定義を「前提」でも「かつ」でも使えます。
CucumberとGherkinの違い
Gherkin は文章の書き方(文法)、Cucumber はその文章を読んで実行する道具です。 Gherkin の文法は Cucumber 以外のツール(Python の behave、.NET の Reqnroll など)でも使われています。
対応している言語
公式ドキュメントのインストールのページ(2026年10月29日時点)では、Cucumber の組織が保守する実装として次が挙がっています。
| 言語 | 実装の名前 | 入れ方(主なもの) |
|---|---|---|
| JavaScript・TypeScript | Cucumber-JS | npm の @cucumber/cucumber |
| Java・Kotlin | Cucumber-JVM | Maven・Gradle の io.cucumber |
| Ruby | Cucumber-Ruby | gem の cucumber |
| Scala | Cucumber-Scala | sbt・Maven |
| C++ | Cucumber.cpp | ソースからビルド |
このほか、Go・Rust・Swift・Elixir・R などに、Cucumber の部品を使わない非公式の実装があります。Python では、Gherkin を読む別のツール(behave・pytest-bdd)がよく使われます。この記事は Node.js の Cucumber-JS で動かし、Java の書き方は後の節で短く扱います。
CucumberとBDD(振る舞い駆動開発)の関係
この節では、Cucumber の前提になっている BDD の考え方と、TDD との違いが分かります。
BDD は「テストを書く手法」ではなく、具体例を通して、作る側と頼む側が仕様をそろえる進め方です。 Cucumber の公式ドキュメントは、BDD を次の3つの取り組みに分けています。
Discovery(話し合って例を見つける)
業務の担当者・開発者・テスト担当が集まり、「5,000円ちょうどなら送料は?」のような具体例で決まりを確かめる。抜けや食い違いはここで見つかる。
Formulation(例を文書にする)
見つけた例を、人もコンピューターも読める形(Gherkin)で書く。公式は「人とコンピューターの両方が読める媒体」と表現している。
Automation(例を自動テストにする)
ステップ定義を書いて例を実行できるようにする。最初は振る舞いが作られていないので失敗し、それを手がかりに実装を進める。
Cucumber が担うのは2と3です。1の話し合いをせずに Cucumber だけ入れると、「英語の代わりに日本語で書いた、遅いテストコード」になりやすい点が、多くのチームのつまずきです。
TDD・単体テストとの違い
TDD・単体テスト
読む人は開発者
関数・クラスの入出力を細かく確かめる
テストコードだけで完結する
速い(この記事の例で 0.1〜0.2秒)
BDD・Cucumber
読む人は業務の担当者を含むチーム全員
利用者から見た振る舞いを例で確かめる
文章とコードの2層になる
同じ確認でも起動の分だけ遅い(0.9〜1.0秒)
BDD は TDD(テスト駆動開発)を置き換えるものではなく、外側(受け入れ)を Cucumber、内側(部品)を単体テストで回す組み合わせがよく使われます。
Cucumberのインストール(Node.js)
この節では、Node.js のプロジェクトに Cucumber を入れて、版を確かめるまでが分かります。
Node.js 22 以上で、npm i -D @cucumber/cucumber の1行で入ります。 13.3.0 の package.json の engines は 22 || 24 || >=26 で、Node.js 20 以前は対象外です。
mkdir cucumber-demo && cd cucumber-demo
npm init -y
npm i -D @cucumber/cucumber
npx cucumber-js --version
13.3.0
npm の公開記録では、13.1.1(7月16日)・13.2.0(7月20日)・13.2.1(8月4日)・13.3.0(10月5日)と、2026年は数週間〜2か月おきに版が出ています。チームで版をそろえるために、package-lock.json をリポジトリに入れておきます。
フォルダーの構成は、既定では次の形です。Cucumber は引数を省くと features の下の .feature を読み、同じ features の下の .js をステップ定義として読み込みます。
cucumber-demo/
├── package.json
├── cucumber.cjs … 設定ファイル(後の節で作る)
├── src/cart.js … テストされる側のコード
└── features/
├── cart.feature … 仕様(Gherkin)
├── step_definitions/
│ └── cart.steps.js … ステップ定義
└── support/
└── world.js … World・フック
Gherkinの書き方(日本語キーワード一覧)
この節では、Gherkin のキーワードと、日本語で書くときの決まりが分かります。
ファイルの1行目に # language: ja と書くと、キーワードを日本語で書けます。 書かないと英語として読まれます。Gherkin は70以上の言語に訳されていて、npx cucumber-js --i18n-keywords ja で日本語のキーワードを確かめられます。
ENGLISH KEYWORD | NATIVE KEYWORDS
Feature | "フィーチャ", "機能"
Rule | "ルール"
Background | "背景"
Scenario | "シナリオ"
Scenario Outline | "シナリオアウトライン", "シナリオテンプレート", "テンプレ", "シナリオテンプレ"
Examples | "例", "サンプル"
Given | "* ", "前提"
When | "* ", "もし"
Then | "* ", "ならば"
And | "* ", "且つ", "かつ"
But | "* ", "然し", "しかし", "但し", "ただし"
| 英語 | 日本語 | 役割 |
|---|---|---|
| Feature | 機能 | ファイルに1つ。何の機能かと、その目的を書く |
| Rule | ルール | 1つの業務ルールごとにシナリオをまとめる(Gherkin 6 以降) |
| Background | 背景 | 各シナリオの前に毎回行う「前提」をまとめる |
| Scenario | シナリオ | 1つの具体例。ステップの並び |
| Scenario Outline | シナリオアウトライン | 値だけ変えて何度も回すひな形 |
| Examples | 例 | シナリオアウトラインに入れる値の表 |
| Given / When / Then | 前提 / もし / ならば | 状況 / 操作 / 期待する結果 |
| And / But | かつ / しかし | 直前と同じ種類のステップを続ける |
記号は4つあります。""" は複数行の文章(Doc String)、| は表(データテーブル)、@ はタグ、行頭の # はコメントです。
日本語のキーワードの後ろは空白が無くても動く
一覧の英語のキーワードには空白が含まれますが、日本語の「前提」「もし」などには空白が含まれていません。実際に 前提所持金が 10,000円 ある と詰めて書いても、空白を入れたときと同じステップ定義に一致して passed になりました。pretty 形式の出力は、空白を入れて書いた行も ✔ 前提空のカートがある のように詰めて表示されます。読みやすさのため、この記事では空白を入れて書きます。
1行目の言語指定を忘れたとき
言語の指定が無いファイルに日本語のキーワードを書くと、実行前に構文エラーで止まります。
Parse error in "errs/nolang.feature" (1:1): expected: #EOF, #Language, #TagLine, #FeatureLine, #Comment, #Empty, got '機能: 言語指定なし'
Parse error in "errs/nolang.feature" (2:3): expected: #EOF, #Language, #TagLine, #FeatureLine, #Comment, #Empty, got 'シナリオ: 何か'
Parse error in "errs/nolang.feature" (3:5): expected: #EOF, #Language, #TagLine, #FeatureLine, #Comment, #Empty, got '前提 空のカートがある
1行目に # language: ja を足すか、全部のファイルを日本語で書くなら実行時に --language ja(設定ファイルなら language: 'ja')を付けると直ります。--language ja を付けて同じファイルを動かすと、1 scenario (1 passed) になりました。
最初のテストを動かす(featureファイルとステップ定義)
この節では、Cucumber の使い方の基本として、仕様を書いてから passed になるまでの流れが分かります。
題材は「カートの合計金額」です。商品代金が5,000円未満なら送料500円、5,000円以上なら送料無料という決まりを、例で書きます。
# language: ja
機能: カートの合計金額
買い物かごに入れた商品から、送料込みの支払額を計算する
背景:
前提 空のカートがある
シナリオ: 5,000円未満は送料500円がかかる
もし 単価 1200 円の "ノート" を 2 個入れる
ならば 支払額は 2900 円になる
シナリオ: 5,000円以上は送料が無料になる
もし 単価 2500 円の "ペン" を 2 個入れる
ならば 支払額は 5000 円になる
テストされる側のコード(src/cart.js)です。
class Cart {
constructor() { this.items = []; }
add(name, price, qty = 1) {
if (qty <= 0) throw new Error('数量は1以上で指定します');
this.items.push({ name, price, qty });
}
subtotal() { return this.items.reduce((s, i) => s + i.price * i.qty, 0); }
shipping() { return this.subtotal() >= 5000 ? 0 : 500; }
total(coupon) {
let sub = this.subtotal();
if (coupon === 'WELCOME10') sub = Math.floor(sub * 0.9);
return sub + this.shipping();
}
}
module.exports = { Cart };
ステップ定義(features/step_definitions/cart.steps.js)です。文の型の中の {int} や {string} に当たる部分が、関数の引数として渡されます。
const { Given, When, Then } = require('@cucumber/cucumber');
const assert = require('node:assert/strict');
const { Cart } = require('../../src/cart');
Given('空のカートがある', function () {
this.cart = new Cart();
});
When('単価 {int} 円の {string} を {int} 個入れる', function (price, name, qty) {
this.cart.add(name, price, qty);
});
Then('支払額は {int} 円になる', function (expected) {
assert.equal(this.cart.total(), expected);
});
npx cucumber-js --format pretty で実行した出力です(シナリオアウトラインの分を含みます)。何も付けない既定の形式は、1ステップを1文字(.)で表す progress です。
機能: カートの合計金額
シナリオ: 5,000円未満は送料500円がかかる # features/cart.feature:8
✔ 前提空のカートがある # features/step_definitions/cart.steps.js:5
✔ もし単価 1200 円の "ノート" を 2 個入れる # features/step_definitions/cart.steps.js:9
✔ ならば支払額は 2900 円になる # features/step_definitions/cart.steps.js:13
シナリオ: 5,000円以上は送料が無料になる # features/cart.feature:12
✔ 前提空のカートがある # features/step_definitions/cart.steps.js:5
✔ もし単価 2500 円の "ペン" を 2 個入れる # features/step_definitions/cart.steps.js:9
✔ ならば支払額は 5000 円になる # features/step_definitions/cart.steps.js:13
...
5 scenarios (5 passed)
15 steps (15 passed)
0m 0.076s (0m 0.003s executing your code)
最後の行の前の数字は Cucumber 全体の時間、括弧の中はステップ定義のコードを動かしていた時間です。プロセスの起動を含めた実時間は 0.9 秒でした。
ステップ定義が無いときは雛形が出る
まだステップ定義が無い文を書くと、そのシナリオは undefined になり、貼り付けて使える雛形が表示されます。 クーポンの例を足して動かした出力です。
✔ 前提空のカートがある # features/step_definitions/cart.steps.js:5
✔ もし単価 3000 円の "かばん" を 1 個入れる # features/step_definitions/cart.steps.js:9
■ かつクーポン "WELCOME10" を使う
↷ ならば支払額は 3200 円になる # features/step_definitions/cart.steps.js:13
Undefined scenarios:
1) 10%引きクーポンを使う # errs/undefined.feature:3
かつクーポン "WELCOME10" を使う
1 scenario (1 undefined)
4 steps (2 passed, 1 skipped, 1 undefined)
You can implement missing steps with the snippets below:
When('クーポン {string} を使う', function (string) {
// Write code here that turns the phrase above into concrete actions
return 'pending';
});
「かつ」は直前の「もし」と同じ種類と見なされ、雛形は When で出ました。終了コードは 1 で、CI では失敗として扱われます。雛形を貼り、this.coupon = code; と書いて動かすと、今度は計算の誤りが見つかりました。
✘ ならば支払額は 3200 円になる # features/step_definitions/cart.steps.js:13
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
3500 !== 3200
+ expected - actual
-3500
+3200
at World.<anonymous> (/dev/shm/t155/proj/features/step_definitions/cart.steps.js:14:10)
クーポンを覚えても、「ならば」のステップが total() にクーポンを渡していなかったためです。this.cart.total(this.coupon) に直すと passed になりました。ステップの間で値を受け渡すのに使っている this が、後の節で扱う World です。
Cucumber Expressions({int}・{string}と独自の型)
この節では、ステップ定義の文の型の書き方と、取り出した値の型が分かります。
文の型は Cucumber Expressions という書き方で、{int} などのパラメーター型を置いた場所の値が引数になります。 正規表現(/^…$/)でも書けます。組み込みの型を1つのステップに並べ、受け取った値と JavaScript の型を出力しました。
* 型を見る 3 1.5 りんご "赤い りんご" なんでも 入る
[3,1.5,"りんご","赤い りんご","なんでも 入る"] number,number,string,string,string
| 書き方 | 一致するもの | 受け取った値 |
|---|---|---|
| {int} | 整数 | 3(number) |
| {float} | 小数 | 1.5(number) |
| {word} | 空白を含まない1語 | りんご(string) |
| {string} | "…" か '…' で囲んだ文字列 | 赤い りんご(引用符は外れる) |
| {} | 何でも(匿名) | なんでも 入る(string) |
独自のパラメーター型を作る
「1,200円」のような業務の書き方をそのまま使いたいときは、defineParameterType で型を足します。
const { Given, When, Then, defineParameterType } = require('@cucumber/cucumber');
defineParameterType({
name: 'yen',
regexp: /[\d,]+円/,
transformer: s => Number(s.replace(/[,円]/g, '')),
});
Given('所持金が {yen} ある', function (n) { this.money = n; });
When('{yen} の商品を買う', function (n) { this.money -= n; });
Then('残りは {yen} になる', function (n) { assert.equal(this.money, n); });
シナリオ: 独自のパラメーター型 # x/x.feature:4
✔ 前提所持金が 10,000円 ある # x/steps/x.steps.js:4
✔ もし1,200円 の商品を買う # x/steps/x.steps.js:5
✔ ならば残りは 8,800円 になる # x/steps/x.steps.js:6
feature ファイルには業務の表記(カンマ・円)を残し、変換はステップ定義の側に1か所だけ書けます。
シナリオアウトライン・データテーブル・Doc String
この節では、同じシナリオを値だけ変えて回す方法と、表や複数行の文章をステップに渡す方法が分かります。
シナリオアウトライン(例の表で何度も回す)
境目の値を確かめるときは、シナリオアウトラインと「例」の表を使います。 <単価> のように山かっこで囲んだ名前が、表の同じ名前の列の値に置き換わります。
シナリオアウトライン: 送料無料の境目
もし 単価 <単価> 円の "商品" を <個数> 個入れる
ならば 支払額は <支払額> 円になる
例:
| 単価 | 個数 | 支払額 |
| 4999 | 1 | 5499 |
| 5000 | 1 | 5000 |
| 1000 | 5 | 5000 |
シナリオアウトライン: 送料無料の境目 # features/cart.feature:22
✔ もし単価 4999 円の "商品" を 1 個入れる # features/step_definitions/cart.steps.js:9
✔ ならば支払額は 5499 円になる # features/step_definitions/cart.steps.js:13
シナリオアウトライン: 送料無料の境目 # features/cart.feature:23
✔ もし単価 5000 円の "商品" を 1 個入れる # features/step_definitions/cart.steps.js:9
✔ ならば支払額は 5000 円になる # features/step_definitions/cart.steps.js:13
表の1行が1シナリオとして数えられ、行番号(:22・:23・:24)も表の行を指します。失敗した行だけを npx cucumber-js features/cart.feature:23 のように指定して動かせます。境目の値の選び方は、関連記事「同値分割・境界値分析|テストケース表とpytestコード例で解説」で扱っています。
データテーブル(表をそのまま渡す)
複数の商品を一度に入れるような場面では、ステップの下に表を書きます。ステップ定義の最後の引数に表が渡され、hashes() で「見出し→値」の配列になります。
もし 以下の商品を入れる:
| 商品 | 単価 | 個数 |
| かばん | 3000 | 1 |
| 財布 | 1000 | 1 |
かつ クーポン "WELCOME10" を使う
ならば 支払額は 4100 円になる
When('以下の商品を入れる:', function (dataTable) {
for (const row of dataTable.hashes()) {
this.cart.add(row['商品'], Number(row['単価']), Number(row['個数']));
}
});
表の値はすべて文字列で渡るため、数として使うなら Number() で変換します。このほか rows()(見出しを除いた2次元配列)、raw()(見出しを含む2次元配列)、rowsHash()(2列の表を「キー→値」に)があります。
Doc String(複数行の文章)とルール
メール本文や JSON のような複数行の文章は """ で囲みます。「ルール」は、1つの業務ルールごとにシナリオをまとめる見出しです。
ルール: 複数行の文章は Doc String で渡す
シナリオ: Doc String
前提 次の案内文がある:
"""
ご注文ありがとうございます。
発送は3営業日以内です。
"""
ならば 案内文は 2 行である
Doc String は、""" の行の字下げを基準に左の空白が取り除かれ、2行の文字列としてステップ定義に渡りました(passed)。
フック(Before・After)とWorldの使い方
この節では、シナリオの前後に処理を差し込むフックと、ステップ間で値を共有する World が分かります。
World は、シナリオごとに新しく作られる入れ物で、ステップ定義の中の this がそれを指します。 シナリオが変わると中身は消えるので、前のシナリオの値が残りません。フックは、World の準備やブラウザの起動・後片付けに使います。
const { setWorldConstructor, World, Before, After, BeforeAll, AfterAll,
setDefaultTimeout } = require('@cucumber/cucumber');
setDefaultTimeout(10 * 1000); // 1ステップの待ち時間の上限(既定は5秒)
class ShopWorld extends World {
constructor(options) {
super(options);
this.cart = null;
this.coupon = undefined;
}
}
setWorldConstructor(ShopWorld);
const order = [];
BeforeAll(function () { order.push('BeforeAll'); });
Before(function ({ pickle }) { order.push(`Before: ${pickle.name}`); });
Before({ tags: '@coupon' }, function () { order.push('Before(@coupon だけ)'); });
After(function ({ pickle, result }) { order.push(`After: ${pickle.name} -> ${result.status}`); });
AfterAll(function () { console.log('\n[フックの実行順]\n' + order.join('\n')); });
実行順を記録した出力の一部です。
[フックの実行順]
BeforeAll
Before: 5,000円未満は送料500円がかかる
After: 5,000円未満は送料500円がかかる -> PASSED
...
Before: 10%引きは商品代金だけにかかり、送料にはかからない
Before(@coupon だけ)
After: 10%引きは商品代金だけにかかり、送料にはかからない -> PASSED
2 hooks (2 passed)
7 scenarios (7 passed)
39 steps (39 passed)
| フック | いつ動く | 主な使い道 |
|---|---|---|
| BeforeAll / AfterAll | 全体の最初と最後に1回(並列ならワーカーごと) | サーバー・ブラウザの起動と停止 |
| Before / After | シナリオごと(背景より前・最後のステップの後) | 画面を開く・失敗時のスクリーンショット |
| BeforeStep / AfterStep | ステップごと | ログ・計測 |
| Before({ tags }) | 条件に合うシナリオだけ | @db のときだけデータを入れる |
出力の最後の 2 hooks は BeforeAll と AfterAll の数で、シナリオごとの Before・After は各シナリオのステップ数の中で数えられます。同じ種類のフックは書いた順に動き、@coupon の付いたシナリオでは、条件なしの Before のあとに条件つきの Before が動きました。
ステップ定義の関数の書き方
Given('…', () => { this.cart = … })。アロー関数の this は World にならず、次のステップで Cannot read properties of undefined (reading 'total') になったfunction () { … } で書く。this が World を指し、値をステップ間で受け渡せる非同期の処理も async function () { … } と書けば待ってくれる
タグ・設定ファイル・レポートの出し方
この節では、動かすシナリオを絞る方法、毎回のオプションを設定ファイルにまとめる方法、レポートの種類が分かります。
タグで動かすシナリオを絞る
--tags に and・or・not と括弧を組み合わせた式を書きます。 機能に付けたタグは、その中の全シナリオに引き継がれます。
npx cucumber-js --tags '@coupon and not @slow' # → 1 scenario (1 passed)
npx cucumber-js --name '境目' # 名前の一部(正規表現)で絞る → 3 scenarios
npx cucumber-js features/cart.feature:8 # 行番号で1つだけ
npx cucumber-js --dry-run # ステップを動かさず、定義の有無だけ確かめる
@smoke
短時間で回す最低限のシナリオ。プルリクエストごとに動かす。
--tags @smoke
@slow
時間のかかるもの。夜間だけ動かす。
not @slow で外す
@wip
作業中。CI では外し、手元でだけ動かす。
チームで意味をそろえる
@ui・@api
層ごとに分ける。フックの条件にも使える。
Before({ tags: '@ui' })
設定ファイル(cucumber.cjs)とプロファイル
毎回付けるオプションは、プロジェクト直下の設定ファイルに書きます。-p 名前 で別の組み合わせ(プロファイル)に切り替えられます。
// cucumber.cjs
module.exports = {
default: {
paths: ['features/**/*.feature'],
require: ['features/**/*.js'],
format: ['progress', 'html:reports/cucumber-report.html', 'junit:reports/junit.xml'],
formatOptions: { snippetInterface: 'synchronous' },
},
ui: {
require: ['e2e/features/**/*.js'],
format: ['pretty', 'html:reports/ui-report.html'],
},
};
注意:設定ファイルの paths と、コマンドラインに書いたパスは足し合わされます。 両方に書いて動かすと、13.3.0 では次の警告が出て、設定ファイル側の全 feature も一緒に動きました。
You have specified paths in both your configuration file and as CLI arguments.
In a future major version, the CLI argument will override the configuration file instead of being merged.
To prepare for this change, see https://github.com/cucumber/cucumber-js/blob/main/docs/deprecations.md
Current result: e2e/features/**/*.feature, e2e/features/projector.feature
Future result: e2e/features/projector.feature
require も同じく足し合わされ、設定ファイルに書いた場所と同じステップ定義をコマンドラインの -r で重ねて読むと、同じ文が2つ定義されたことになり ambiguous(後の節)になりました。パスをコマンドラインで渡すプロファイルには paths を書かないでおくと、混乱しません。
レポートの種類
--format 形式:出力先 で、複数の形式を同時に出せます。
| 形式 | 出力 | この環境で確かめたこと |
|---|---|---|
| progress(既定) | 1ステップ1文字 | ............... のように並ぶ |
| pretty | Gherkin の文と結果を1行ずつ | ステップ定義の場所も表示 |
| html | 1ファイルで開けるレポート | 約1MB(974〜990KB)。失敗時の画像も中に入った |
| junit | JUnit 形式の XML | CI の集計に渡せる。tests="7" failures="0" |
| rerun | 失敗したシナリオの場所 | rr/undefined.feature:3 の1行 |
| usage | ステップ定義ごとの使用箇所と時間 | 遅い定義から順に表で出る |
rerun 形式のファイルを引数に渡すと、失敗したシナリオだけを動かし直せます。
npx cucumber-js --format progress --format rerun:@rerun.txt
cat @rerun.txt # → rr/undefined.feature:3
npx cucumber-js @rerun.txt # 失敗した1つだけ → 1 scenario (1 failed)
なお、--publish を付けると、レポートが Cucumber の公開サービス(reports.cucumber.io)に送られます。社内の仕様が書かれた feature を外に出さないよう、レポートは手元の html か CI の成果物として残すのが安全です。
PlaywrightとCucumberで受け入れテストを書く
この節では、Cucumber からブラウザを操作して、画面の受け入れテストを動かす方法と、その実行結果が分かります。
Cucumber にはブラウザを動かす機能はありません。BeforeAll でブラウザを起動し、Before でシナリオごとに新しいページを開き、ステップ定義から Playwright で操作します。 題材は、127.0.0.1 で起動した自作の「備品貸出」の画面です。社員番号(5桁の数字)と備品を選んで申請すると、受付番号が表示されます。
# language: ja
@ui
機能: 備品の貸出申請
社員として、備品を画面から申請したい
なぜなら、総務に口頭で頼まなくても借りられるようにしたいから
背景:
前提 備品貸出の画面を開いている
シナリオ: 正しく入力すると受付番号が出る
もし 社員番号に "10234" を入力する
かつ 備品で "ノートPC" を選ぶ
かつ 申請ボタンを押す
ならば "申請を受け付けました(受付番号 L001)" と表示される
かつ 貸出一覧に "ノートPC:10234" が並ぶ
シナリオアウトライン: 入力の誤りはその場で知らせる
もし 社員番号に "<社員番号>" を入力する
かつ 備品で "<備品>" を選ぶ
かつ 申請ボタンを押す
ならば "<メッセージ>" と表示される
例:
| 社員番号 | 備品 | メッセージ |
| 1234 | ノートPC | 社員番号は5桁の数字で入力します |
| abcde | ノートPC | 社員番号は5桁の数字で入力します |
| 10234 | 選んでください | 備品を選びます |
フック(e2e/features/support/hooks.js)です。失敗したときだけスクリーンショットを撮り、this.attach でレポートに添付します。
const { BeforeAll, AfterAll, Before, After, setDefaultTimeout, Status } = require('@cucumber/cucumber');
const { chromium } = require('playwright-core');
const { start, reset } = require('../../app/server');
setDefaultTimeout(15 * 1000);
let browser, server;
BeforeAll(async function () {
server = await start(0); // 127.0.0.1 の空いているポート
process.env.APP_URL = `http://127.0.0.1:${server.address().port}/`;
browser = await chromium.launch({ executablePath: process.env.CHROME_PATH, headless: true });
});
Before(async function () {
reset(); // アプリのデータを毎回空にする(次の節)
this.context = await browser.newContext({ locale: 'ja-JP' });
this.page = await this.context.newPage();
});
After(async function ({ result }) {
if (result.status === Status.FAILED) {
this.attach(await this.page.screenshot(), 'image/png');
}
await this.context.close();
});
AfterAll(async function () {
await browser.close();
await new Promise(r => server.close(r));
});
ステップ定義では、画面の部品をラベルや役割(role)で探します。CSS のクラス名に頼らないので、見た目を変えてもテストが壊れにくくなります。
Given('備品貸出の画面を開いている', async function () {
await this.page.goto(process.env.APP_URL);
});
When('社員番号に {string} を入力する', async function (v) {
await this.page.getByLabel('社員番号').fill(v);
});
When('備品で {string} を選ぶ', async function (v) {
await this.page.getByLabel('備品').selectOption({ label: v });
});
When('申請ボタンを押す', async function () {
await this.page.getByRole('button', { name: '申請する' }).click();
});
Then('{string} と表示される', async function (text) {
const status = this.page.getByRole('status');
try {
await status.filter({ hasText: text }).waitFor({ timeout: 3000 });
} catch (e) {
assert.equal(await status.textContent(), text); // 実際の文言を失敗の理由に出す
}
});
機能: 備品の貸出申請
@ui
シナリオ: 正しく入力すると受付番号が出る # e2e/features/loan.feature:10
✔ 前提備品貸出の画面を開いている # e2e/features/steps/loan.steps.js:4
✔ もし社員番号に "10234" を入力する # e2e/features/steps/loan.steps.js:7
✔ かつ備品で "ノートPC" を選ぶ # e2e/features/steps/loan.steps.js:10
✔ かつ申請ボタンを押す # e2e/features/steps/loan.steps.js:13
✔ ならば"申請を受け付けました(受付番号 L001)" と表示される # e2e/features/steps/loan.steps.js:16
✔ かつ貸出一覧に "ノートPC:10234" が並ぶ # e2e/features/steps/loan.steps.js:21
...
2 hooks (2 passed)
4 scenarios (4 passed)
29 steps (29 passed)
0m 3.239s (0m 3.091s executing your code)
14シナリオで約3.2秒
ブラウザの起動を含めた実時間は 4.8 秒。カートの計算(5シナリオ・0.08秒)の約40倍かかった。
2失敗の画像がレポートに入る
html レポートの中に image/png の添付が1件入り、1ファイルのまま開けた。
3待ち時間切れは原文が分かりにくい
waitFor だけだと Timeout 3000ms exceeded しか出ない。実際の文言を比べて出すと原因がすぐ分かった。
Playwright 単体での書き方(ロケーター・自動待機・CI での実行)は、関連記事「Playwright入門|E2Eテストの書き方とCI実行まで実行結果つきで解説」で扱っています。Cucumber と組み合わせるのは、文章の仕様を業務の担当者と共有したいときです。開発者だけが読むなら、Playwright Test だけで書くほうが部品が少なく済みます。
シナリオを独立させる(前のシナリオのデータが残る失敗)
アプリ側のデータは World と違ってシナリオをまたいで残るため、順番によって結果が変わるテストになりがちです。 データを空にする reset() を入れる前に、プロジェクターの貸出を2つのシナリオで確かめたときの失敗です。
Failed scenarios:
1) 20001 さんがプロジェクターを借りる # e2e/features/projector.feature:13
ならば"申請を受け付けました(受付番号 L001)" と表示される # e2e/features/steps/loan.steps.js:16
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ 'プロジェクターは貸出中です'
- '申請を受け付けました(受付番号 L001)'
2 scenarios (1 passed, 1 failed)
1つ目のシナリオで借りたプロジェクターが、2つ目のシナリオでも「貸出中」のまま残っていました。受付番号も前のシナリオから続いていたため、別の feature と一緒に動かすと、先に動いたシナリオが L001 を使ってしまい、1つ目のシナリオも期待した文言が出ずに失敗しました。Before で reset() を呼ぶと、順番を変えても結果が同じになりました。
| 動かし方 | 結果 | 実時間(3回) |
|---|---|---|
| --order defined(書いた順) | 6 scenarios (6 passed) | 6.4〜8.2秒 |
| --order random:7(乱数の種7) | 6 scenarios (6 passed) | ― |
| --parallel 2(2ワーカー) | 6 scenarios (6 passed) | 6.6〜7.4秒 |
| --order reverse | 設定の検証エラーで動かず | ― |
2 CPU のこのマシンでは、ワーカーごとにブラウザを起動するため、--parallel 2 にしても速くなりませんでした。公式ドキュメントのとおり、並列実行では BeforeAll・AfterAll がワーカーごとに動きます。また、13.3.0 の --help には --order に reverse が載っていますが、指定すると order must match the following: "/^random:.*|random|defined$/" の検証エラーになりました。
よくあるエラー・つまずき(原文つき)
この節では、Cucumber でよく出るエラーの原文と、原因・直し方が一覧で分かります。
終了コードは、すべて passed のときだけ 0 で、undefined・pending・ambiguous・failed が1つでもあると 1 でした。
| 出る文言(原文) | 原因 | 直し方 |
|---|---|---|
Undefined scenarios: と You can implement missing steps with the snippets below: |
文に一致するステップ定義が無い。読み込む場所(require)の外に置いた場合も同じ | 雛形を貼る。設定の require のパスを確かめる |
Multiple matching step definitions found:(結果は ambiguous) |
同じ文に一致する定義が2つ。設定と -r で同じファイルを2回読んだ場合も出る | 片方を消すか、文の型を具体的にする |
Parse error in "…" (1:1): expected: #EOF, #Language, … |
日本語のキーワードなのに言語の指定が無い。コロン抜け・字下げの崩れでも出る | 1行目に # language: ja |
Pending scenarios: |
雛形の return 'pending' が残っている |
中身を書く。作業中なら @wip で外す |
TypeError: Cannot read properties of undefined |
アロー関数で this が World にならない、または前のステップで値を入れていない | function () {} で書く |
locator.waitFor: Timeout 3000ms exceeded. |
期待した文言・要素が画面に出ない | 実際の文言を出して比べる。ロケーターを見直す |
You have specified paths in both your configuration file and as CLI arguments. |
設定の paths と引数のパスが足し合わされた | どちらか一方に書く |
Provided configuration value failed schema validation: order must match … |
--order に reverse など受け付けない値 | defined か random(random:種) |
このほか、1ステップが既定の5秒を超えると function timed out, ensure the promise resolves within 5000 milliseconds で失敗します。ブラウザを使うテストでは setDefaultTimeout で長めにするか、ステップ定義に { timeout: 15000 } を渡します。
ステップ定義を書くときのチェックリスト
- 1関数は function で書くアロー関数では this が World にならない
- 21つの文に1つの定義似た正規表現を足すと ambiguous になる。--dry-run で先に確かめる
- 3値はパラメーター型で受ける{int} なら数値で届く。表の値は文字列なので Number() で変換する
- 4シナリオごとにデータを戻すBefore で DB やアプリの状態を初期化し、--order random でも通るか見る
- 5失敗の理由に実際の値を出す待ち時間切れだけでなく、画面に出ていた文言を比べて出す
良いシナリオの書き方(宣言的に書く)
シナリオには「何をしたいか(業務の言葉)」を書き、「どのボタンを押すか(画面の手順)」はステップ定義に閉じ込めます。 画面の手順を feature に書くと、画面を少し変えただけで仕様書の書き直しが必要になります。
同じ「申請できる」を書く
業務の担当者が読んで「その通り」と言えるかで判断する
- 1シナリオ1つの目的:「ならば」が何個も続くなら、シナリオを分ける。
- 前提・もし・ならばの順:「もし」の後にまた「前提」を書かない。状況→操作→結果の1往復にする。
- 具体的な値で書く:「正しい社員番号」ではなく「10234」。境目の値は例の表に並べる。
- 背景は短く:全シナリオに共通の前提だけ。長くなったら機能を分ける。
- 機能の説明に目的を書く:「〜として、〜したい。なぜなら〜」の形はユーザーストーリーと同じ。
「〜として、〜したい」の書き方と受け入れ基準の例は、関連記事「ユーザーストーリーの書き方と例30選|受け入れ基準Given-When-Then」で扱っています。
Java(Cucumber-JVM)での書き方
この節では、Java で Cucumber を使うときの依存・ステップ定義・実行クラスの形が分かります(公式ドキュメントで確かめた範囲)。
Cucumber-JVM は 8.0.0(2026年9月24日)から Java 17 以上が必要になり、JUnit 4 用の cucumber-junit と cucumber-testng は削除予定(非推奨)になりました。 今から始めるなら JUnit Platform(JUnit 5 以降)で動かす cucumber-junit-platform-engine を使います。CHANGELOG では、2026年10月29日時点の最新は 8.0.4(10月2日)です。7系も 7.34.6 まで保守のリリースが出ています。
<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>8.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit-platform-engine</artifactId>
<version>8.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
公式は「Cucumber の依存はすべて同じ版にそろえる」と注意しています。版をそろえる手間を減らすため、cucumber-bom と junit-bom を dependencyManagement で読み込む方法が案内されています(junit-platform-suite に版を書いていないのはそのためです)。ステップ定義は、注釈(アノテーション)で文の型を書きます。
package com.example;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CartSteps {
private Cart cart;
@Given("空のカートがある")
public void emptyCart() { cart = new Cart(); }
@When("単価 {int} 円の {string} を {int} 個入れる")
public void addItem(int price, String name, int qty) { cart.add(name, price, qty); }
@Then("支払額は {int} 円になる")
public void totalIs(int expected) { assertEquals(expected, cart.total()); }
}
feature ファイルが日本語でも、ステップ定義の注釈は英語の @Given・@When・@Then で書けます。結びつくのはキーワードではなく文の型だけだからです。実行用のクラスは JUnit Platform の Suite で書きます(cucumber-junit-platform-engine の README の例)。
package com.example;
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")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example")
class RunCucumberTest {
}
feature ファイルは src/test/resources/com/example/ に置き、mvn test で動かします。JS 版と同じ feature ファイルをそのまま使えるのが Gherkin の利点です。JUnit 単体での書き方は、JUnit の使い方の記事が回遊先になります。
Cucumberを使うべきか(単体テストとの違いと判断表)
この節では、Cucumber が向く場面・向かない場面と、入れるときの順番が分かります。
Cucumber が役に立つのは、仕様を業務の担当者と文章でそろえたいときです。 開発者だけが読むテストに入れると、文章とコードの2層を保つ手間だけが増えます。同じ5つのカートの確認で比べると、node:test は 0.1〜0.2秒、Cucumber は 0.9〜1.0秒(どちらもプロセスの起動を含む実時間)でした。
| 観点 | Cucumber | 単体テスト(Jest・node:test・JUnit) |
|---|---|---|
| 読む人 | 業務の担当者・テスト担当・開発者 | 開発者 |
| 書く単位 | 利用者から見た振る舞い(例) | 関数・クラスの入出力 |
| ファイル | feature+ステップ定義の2つ | テストコードの1つ |
| 速さ(この記事の例) | 0.9〜1.0秒(5シナリオ) | 0.1〜0.2秒(5ケース) |
| 向く層 | 受け入れテスト・E2E・API | 単体・結合 |
| 場面 | 判断 | 理由 |
|---|---|---|
| 業務の決まりが複雑で、担当者と例で合わせたい | 向く | feature がそのまま合意した仕様書になる |
| 受け入れテストを業務の担当者が確認する | 向く | 結果が文章のまま読める(html レポート) |
| 同じ仕様を Java と JS の両方で確かめる | 向く | feature を共有できる |
| 開発者だけで、細かい計算や分岐を確かめる | 向かない | 単体テストのほうが速く短い |
| 仕様を話し合う相手がいない | 向かない | Gherkin が読まれず、手間だけが増える |
| 画面の手順を全部書きたい | 向かない | 命令的な feature は壊れやすい。Playwright Test 単体で書く |
受け入れテスト全体の観点(何を確かめるか、どこまでを合格とするか)は、関連記事「受入テストの観点チェックリストとUATシナリオの書き方」で扱っています。単体テストと結合テストの分け方は、関連記事「単体テストと結合テストの違い|pytestの実例と実行結果で比較」が参考になります。
入れるときの順番
- 11つの機能で例を話し合う境目の値(5,000円ちょうど等)を3〜5個、担当者と決める
- 2feature を1本書き、担当者に読んでもらう「その通り」と言われるまで言葉を直す
- 3ステップ定義を書いて passed にするまずは関数や API を直接呼ぶ。画面は後
- 4CI で動かし、html・junit のレポートを残す終了コード 1 でビルドを止める
- 5必要な分だけ画面のテストを足すPlaywright をフックで起動。@ui のタグで分ける
よくある質問
Q. Cucumberとは何ですか? A. Gherkin という決まった形の文章(前提・もし・ならば)で書いた仕様を、自動テストとして実行するツールです。BDD(振る舞い駆動開発)を支えるために作られ、JavaScript・Java・Ruby などに実装があります。
Q. CucumberとGherkinの違いは何ですか? A. Gherkin は仕様を書くための文法、Cucumber はその文章を読んでステップ定義のコードを動かす道具です。Gherkin は behave や Reqnroll など Cucumber 以外のツールでも使われます。
Q. Gherkinは日本語で書けますか?
A. 書けます。ファイルの1行目に # language: ja と書き、「機能」「シナリオ」「前提」「もし」「ならば」「かつ」を使います。指定を忘れると Parse error … expected: #EOF, #Language … で止まります。
Q. Cucumberの使い方を最短で知りたいです。
A. npm i -D @cucumber/cucumber で入れ、features フォルダーに .feature を置いて npx cucumber-js を動かします。ステップ定義が無い文には雛形が出るので、それを features/step_definitions に貼って中身を書きます。
Q. Undefined と表示されて動きません。
A. その文に一致するステップ定義が見つかっていません。雛形を貼っていない、または定義のファイルが読み込む場所(設定の require・-r)の外にあることが原因です。--dry-run で、動かさずに定義の有無だけを確かめられます。
Q. ステップ定義で this が undefined になります。
A. アロー関数で書いていると、this がシナリオごとの World になりません。function () { … } か async function () { … } で書きます。
Q. Cucumberでブラウザのテストはできますか? A. Cucumber 自体にはブラウザの操作機能が無いので、Playwright や Selenium を組み合わせます。BeforeAll でブラウザを起動し、Before でシナリオごとにページを開き、After で閉じます。
Q. シナリオアウトラインとデータテーブルはどう使い分けますか? A. シナリオアウトラインは「同じシナリオを値だけ変えて何回も動かす」もので、表の1行が1シナリオになります。データテーブルは「1つのステップに表をまとめて渡す」もので、シナリオは1つのままです。
Q. Cucumberのテストは遅いですか? A. この記事の例では、同じ5件の確認で node:test が 0.1〜0.2秒、Cucumber が 0.9〜1.0秒でした。差の多くは起動の分です。細かい計算は単体テストで書き、Cucumber は受け入れの例に絞ると全体が遅くなりません。
Q. JavaでCucumberを使うときの注意は? A. Cucumber-JVM 8.0 からは Java 17 以上が必要で、JUnit 4 用の cucumber-junit は非推奨です。cucumber-junit-platform-engine と junit-platform-suite を使い、Cucumber の依存の版はすべてそろえます。
Q. Cucumberのレポートを共有したいです。
A. --format html:reports/report.html で1ファイルの html が出せ、失敗時のスクリーンショットも中に入ります。--publish は外部のサービスにレポートを送るので、社内の仕様を含む場合は使わず、CI の成果物として残します。
まとめ
- Cucumber は、Gherkin で書いた「前提・もし・ならば」の例を、ステップ定義のコードに結びつけて自動テストとして動かすツール。BDD の「例を文書にする」「自動化する」を担う。
- 日本語は1行目の
# language: jaで書ける。忘れると Parse error。キーワードの後ろは空白が無くても動いた。 - 文に一致する定義が0なら undefined(雛形が出る)、2つ以上なら ambiguous。設定ファイルと CLI の paths・require は足し合わされる点に注意。
- World はシナリオごとに作り直されるが、アプリや DB のデータは残る。Before で初期化し、--order random でも通るか確かめる。ステップ定義は function で書く。
- Cucumber が役に立つのは、仕様を業務の担当者と例でそろえたい受け入れテスト。開発者だけで細かく確かめるなら、単体テストのほうが速く短い。
参考・出典
確認日はいずれも 2026年10月29日です。
- Cucumber「Behaviour-Driven Development」:https://cucumber.io/docs/bdd/
- Cucumber「Gherkin Reference」:https://cucumber.io/docs/gherkin/reference/
- Cucumber「Installation」:https://cucumber.io/docs/installation/
- Cucumber「Java」(Installation):https://cucumber.io/docs/installation/java/
- cucumber-js「docs/parallel.md」:https://github.com/cucumber/cucumber-js/blob/main/docs/parallel.md
- cucumber-js「docs/deprecations.md」:https://github.com/cucumber/cucumber-js/blob/main/docs/deprecations.md
- npm「@cucumber/cucumber」:https://www.npmjs.com/package/@cucumber/cucumber
- cucumber-jvm「CHANGELOG.md」:https://github.com/cucumber/cucumber-jvm/blob/main/CHANGELOG.md
- cucumber-jvm「cucumber-junit-platform-engine README」:https://github.com/cucumber/cucumber-jvm/tree/main/cucumber-junit-platform-engine
- Playwright「Locators」:https://playwright.dev/docs/locators
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Cucumberとは?使い方を日本語Gherkinの実行結果で解説【BDD】」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。