このチュートリアルでは BDD フレームワーク Cucumber の Ruby 版チュートリアル「Is it Friday yet?」をもとに,Rust で同様のテストを再現します.
このチュートリアルの最終的なプロジェクト構造は次のようになります.
hello-cucumber/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
├── bdd.rs
└── features/
└── is_it_friday_yet.feature
そもそもBDDとは?
BDD (Behavior Driven Development) は「ソフトウェアの仕様」を「自然言語で書いた例(=シナリオ)」として定義し,それを自動テストとして検証する手法です.
Cucumber では開発者・非エンジニア(マネジメントやビジネスサイドなど)間の共通言語として .feature ファイル(Gherkin 構文)を使い期待する挙動をコードに落とし込みます.
プロジェクトを作成
まずは新しいライブラリプロジェクトを作成します.
cargo new hello-cucumber --lib
cd hello-cucumber
次に依存関係を追加します.
# Cargo.toml
[package]
name = "hello-cucumber"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1.48.0", features = ["full"] }
[dev-dependencies]
cucumber = { version = "0.21.1", features = ["macros"] }
[[test]]
name = "bdd"
harness = false
cucumber は BDD を Rust で実現するためのライブラリです.
tokio は非同期処理を行うためのライブラリです.cucumber は実行を非同期(async)前提で設計しています.
これは BDD テストがしばしば
- HTTP API の呼び出し
- DB アクセス
- ファイル I/O
などの外部システムとの連携を含むため、非同期処理が自然に登場することを想定しているためです.
[[test]] harness = false により通常のテストランナーを無効にし cucumber がメイン関数から起動できるようにします.
テスト仕様(Feature)を書く
Rustコードを書く前に「どんな振る舞いを期待しているのか」を自然言語で書きます.
ここではテスト対象は
「(前提条件として)今日が日曜日のとき,"今日は金曜日ですか?" と質問すると,"いいえ"と返す」
という振る舞いをするものとします.
これを tests/features/is_it_friday_yet.feature に記述するとき,次のように書くことができます.
# tests/features/is_it_friday_yet.feature
Feature: Is it Friday yet?
Everybody wants to know when it's Friday
Scenario: Sunday isn't Friday
Given today is Sunday
When I ask whether it's Friday yet
Then I should be told "Nope"
ここで,それぞれの項目は
-
Feature: テスト対象の機能の名前 -
Scenario: テストシナリオ名 -
Given: 事前条件 -
When: アクション -
Then: 期待する結果
を表します.
Feature の次の行にある「Everybody wants to know when it's Friday」は Feature に関する説明文です.
テスト対象の関数を用意
cucumber でテストする対象の関数を用意します.
まずは仕様通りでない実装がテストに失敗することを確認するため,空の文字列を返すようにします.
// src/lib.rs
pub fn is_it_friday(day: &str) -> String {
String::new()
}
共通設定と Test Harness を作成
次に Rust 側のテスト実装を作成します.まずはテストの共通設定(World)を定義します.
// tests/bdd.rs
use cucumber::World;
use tokio;
#[derive(cucumber::World, Debug, Default)]
struct FridayWorld {
today: String,
answer: String,
}
#[tokio::main]
async fn main() {
FridayWorld::run("tests/features").await;
}
解説
-
FridayWorldは「テストシナリオの状態(コンテキスト)」を持つ構造体- ここでは
"today"と"answer"という 2 つの値を管理
- ここでは
-
#[derive(cucumber::World)]によって cucumber がこの構造体を「テストの状態」として扱えるようになる -
#[tokio::main]は非同期のエントリーポイントを定義するマクロ -
FridayWorld::run("tests/features")によって,指定した.featureディレクトリを読み込み,ステップ定義と照らし合わせてテストを実行
実行
$ cargo test --test bdd
warning: fields `today` and `answer` are never read
--> tests/bdd.rs:7:5
|
6 | struct FridayWorld {
| ----------- fields in this struct
7 | today: String,
| ^^^^^
8 | answer: String,
| ^^^^^^
|
= note: `FridayWorld` has derived impls for the traits `Default` and `Debug`, but these are intentionally ignored during dead code analysis
= note: `#[warn(dead_code)]` on by default
warning: `hello-cucumber` (test "bdd") generated 1 warning
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.07s
Running tests/bdd.rs (target/debug/deps/bdd-45e2a9367f121b93)
Feature: Is it Friday yet?
Scenario: Sunday isn't Friday
? Given today is "Sunday"
Step skipped: tests/features/is_it_friday_yet.feature:6:5
[Summary]
1 feature
1 scenario (1 skipped)
1 step (1 skipped)
この段階ではまだ Given / When / Then を実装していないため,テストシナリオはスキップされます.
Gherkin ステップ(Given / When / Then)を実装
次に .feature ファイルに書いた,Gherkin ステップ(Given / When / Then)に対応する Rust 関数を追加します.
// tests/bdd.rs
use cucumber::{given, when, then, World as _};
use hello_cucumber::is_it_friday;
#[derive(cucumber::World, Debug, Default)]
struct FridayWorld {
today: String,
answer: String,
}
#[given(expr = "today is {word}")]
async fn today_is(w: &mut FridayWorld, day: String) {
w.today = day;
}
#[when("I ask whether it's Friday yet")]
async fn i_ask(w: &mut FridayWorld) {
w.answer = is_it_friday(&w.today);
}
#[then(expr = "I should be told {string}")]
async fn i_should_be_told(w: &mut FridayWorld, expected: String) {
assert_eq!(w.answer, expected);
}
#[tokio::main]
async fn main() {
FridayWorld::run("tests/features").await;
}
解説
-
#[given(...)],#[when(...)],#[then(...)]は,それぞれの Gherkin ステップと Rust 関数を結びつける属性マクロ.expr = "{string}"のように文字列を動的に取り出す -
{word}は引用符なし,{string}は引用符ありにマッチする - 各関数は
async fn.cucumber が非同期処理をサポート -
is_it_friday()は冒頭で作成したテスト対象の関数
実行
$ cargo test --test bdd
Blocking waiting for file lock on build directory
Compiling hello-cucumber v0.1.0 (/path/to/hello-cucumber)
warning: unused variable: `day`
--> src/lib.rs:1:21
|
1 | pub fn is_it_friday(day: &str) -> String {
| ^^^ help: if this is intentional, prefix it with an underscore: `_day`
|
= note: `#[warn(unused_variables)]` on by default
warning: `hello-cucumber` (lib) generated 1 warning
Finished `test` profile [unoptimized + debuginfo] target(s) in 2.61s
Running tests/bdd.rs (target/debug/deps/bdd-45e2a9367f121b93)
Feature: Is it Friday yet?
Scenario: Sunday isn't Friday
✔ Given today is Sunday
✔ When I ask whether it's Friday yet
✘ Then I should be told "Nope"
Step failed:
Defined: tests/features/is_it_friday_yet.feature:8:5
Matched: tests/bdd.rs:22:1
Step panicked. Captured output: assertion `left == right` failed
left: ""
right: "Nope"
[Summary]
1 feature
1 scenario (1 failed)
3 steps (2 passed, 1 failed)
現在はテスト対象の is_it_friday() は空の文字列を返す実装になっているためテストは失敗します.
テストを通過するように修正
is_it_friday() に正しいロジックを実装してテストを通過させます.
day == "Friday" のときだけ "TGIF" を返し,それ以外は "Nope" を返すようにします.
// src/lib.rs
pub fn is_it_friday(day: &str) -> String {
if day == "Friday" { "TGIF" } else { "Nope" }.into()
}
実行
すべてのステップに通過します.
$ cargo test --test bdd
Blocking waiting for file lock on package cache
Blocking waiting for file lock on shared package cache
Compiling hello-cucumber v0.1.0 (/Users/masafumi.harada/workspace/hello-cucumber)
Finished `test` profile [unoptimized + debuginfo] target(s) in 3.32s
Running tests/bdd.rs (target/debug/deps/bdd-45e2a9367f121b93)
Feature: Is it Friday yet?
Scenario: Sunday isn't Friday
✔ Given today is "Sunday"
✔ When I ask whether it's Friday yet
✔ Then I should be told "Nope"
[Summary]
1 feature
1 scenario (1 passed)
3 steps (3 passed)
テストシナリオを増やして動作確認
同様にテストシナリオを追加することで既存のステップの実装を再利用できます.
試しに
「(前提条件として)今日が金曜日のとき,"今日はもう金曜日ですか?" と質問すると,"TGIF"と返す」
という振る舞いを確認するテストシナリオを追加します.
「TGIF」は英語の略語で "Thank God It's Friday"(「よっしゃ,金曜日だ!」)の意味です.
# tests/features/is_it_friday_yet.feature
Feature: Is it Friday yet?
Everybody wants to know when it's Friday
Scenario: Sunday isn't Friday
Given today is Sunday
When I ask whether it's Friday yet
Then I should be told "Nope"
# 以下を追加
Scenario: Friday is Friday
Given today is Friday
When I ask whether it's Friday yet
Then I should be told "TGIF"
実行
追加したテストシナリオに対しても合格していることが確認できます.
$ cargo test --test bdd
Compiling hello-cucumber v0.1.0 (/path/to/hello-cucumber)
Finished `test` profile [unoptimized + debuginfo] target(s) in 3.70s
Running tests/bdd.rs (target/debug/deps/bdd-45e2a9367f121b93)
Feature: Is it Friday yet?
Scenario: Sunday isn't Friday
✔ Given today is Sunday
✔ When I ask whether it's Friday yet
✔ Then I should be told "Nope"
Scenario: Friday is Friday
✔ Given today is Friday
✔ When I ask whether it's Friday yet
✔ Then I should be told "TGIF"
[Summary]
1 feature
2 scenarios (2 passed)
6 steps (6 passed)