本記事はアプレッソ Advent Calendar 2017 14日目の記事です。
BDDとは
Behavior Driven Development、振る舞い駆動開発の略です。ソフトウェアの開発手法の改善という文脈で登場します。TDD(テスト駆動開発)よりも一層要求分析フェーズ寄りの開発手法です。
TDDやBDDがソフトウェアの開発手法の中でどんな意味を持っているのかについては、以下の記事が参考になります。
ビヘイビア駆動開発 ― ウォーターフォールモデルからのステップ
要は機能に着目して要求分析、設計、コーディング、テストのサイクルをどんどん回していきましょう、それによってより速く(アジャイルに)開発してリリースしましょう、という考え方です。
JavaのBDDフレームワーク
JavaのBDDフレームワークを調べてみると、いろいろあるようです。
Cucumber
Javaに限らずいろいろな言語で使用可能なBDDの定番フレームワーク。開発者、テスター、ビジネス関係者?(business folks)が知識を共有できる事がメリットだとか。Gherkinという独自の言語によって機能(振る舞い)を記述します。
ちなみにGherkinの日本語読みは「ガーキン」で、ピクルスにする小さいキュウリだそうです。
本家サイト
JBehave
こちらはJavaのみのようですが、Cucumberと同じぐらい人気があるフレームワークのようです。Sinario(シナリオ、ユーザーストーリー), Given(前提条件), When(テストケース),Then(あるべき振る舞い)形式で機能(テスト)を記述できることが特徴です。
本家サイト
例
Sinario:"株価アラートのテスト。与えられた株価がしきい値を超えているかどうかを判断するテスト。"
Given:"株価のしきい値が1500円とする"
When:"株価が500円の場合"
Then:"アラートの状態はOFFである"
When:"株価が1600円の場合"
Then:"アラートの状態はONである"
JGiven
こちらは同じアプレッソの技術者の@enkさんが昨年書いた記事に詳しく書いてあるので、説明はそちらへ譲ります。
Javaだけですべての作業が完結するところが利点のようです。
JBehaveのようにGiven, When, Thenの発想でJUnitが書けるツールといったところでしょうか。
Spectrum
今回紹介したいJavaのBDDフレームワークです。どちらかというとJGiven寄りの位置づけだと思います。
GitHub
特徴
- Javaだけで記述できる(振る舞いのための別ファイルなどは不要)
- 既存のJUnitツールやIDEとシームレスに統合可能
- Specificationスタイル、Gherkinスタイルのどちらでも記述可能
- JunitのテストとSpectrumのテストを混在させることもできる
なぜSpectrumか
ズバリ、導入しやすいからです。
CucumberやJBehaveは振る舞いをテキストファイルで記述し、最終的にJUnitに組み込むというものです。これはCucumberが想定している3種の登場人物(開発者、テスター、ビジネス関係者?プロダクトオーナー?)のうち、最後の人も参加させる事を想定してのことでしょう。大規模な開発で「ちゃんと」BDDを回すにはいいかも知れません。その分用意するものや覚えることが多く、誰にとっても敷居が高いと思います。
JGivenはもっと開発者寄りのフレームワークで、振る舞いをJavaのコードで表現できるというものです。開発者がBDDの発想でテストを書くことを重視したものです。もちろんテストレポートも機能に着目した形式で出力されますので、開発者以外の登場人物も恩恵を受けることができます。しかし、テストクラス以外にState、Action、Outcomeクラスを作らなければならず、また、既存のテストと混在させることは(確認してませんが)難しそうです。
Spectrumの実体はJUitの拡張なので、テストクラス以外のクラスを作る必要はなく、また、既存のJUnitのテストとの混在が可能なため、部分的に導入することができます。
導入方法
クラスパスにspectrumのjarファイルを追加します。
必要条件
- Java 8 (ラムダを使うため)
- JUnit (JUnitの拡張なので)
依存関係の追加
Gradle
build.gradleのdependenciesに以下の1行を追加します。
java-libraryの場合
testImplementation 'com.greghaskins:spectrum:1.2.0'
それ以外の場合
testCompile 'com.greghaskins:spectrum:1.2.0'
Maven
pom.xmlのdependenciesに以下を追加します。
<dependency>
<groupId>com.greghaskins</groupId>
<artifactId>spectrum</artifactId>
<version>1.2.0</version>
<scope>test</scope>
</dependency>
それ以外
Maven Central Repositoryからダウンロードしてクラスパスに追加する。
サンプルコード
テスト対象コードを用意し、Spec形式、Gherkin形式、Junit形式の3種類でテストを作成し、比較してみました。
テスト対象コード
以下のようなテスト対象を用意します。
public class StockWatcher {
private int threshold;
public int getThreshold() {
return threshold;
}
public void setThreshold(int threshold) {
this.threshold = threshold;
}
public boolean getAlert(int stockPrice) {
return threshold <= stockPrice;
}
}
Spec形式のテスト
Spec形式でテストを書くと、以下のように書けます。
import static org.junit.Assert.*;
import static com.greghaskins.spectrum.dsl.specification.Specification.*; // Sectrum固有のもの
import java.util.function.Supplier;
import org.junit.runner.RunWith;
import com.greghaskins.spectrum.Spectrum; // Sectrum固有のもの
/**
* Specification形式のテスト
* describe - it 形式でテストを記述できる
*/
@RunWith(Spectrum.class)
public class StockWatcherSpecTest {{
// describeで仕様を記述する
describe("株価アラートのテスト。与えられた株価がしきい値を超えているかどうかを判断するテスト。", ()->{
// describeは入れ子にできる
describe("株価のしきい値が1500円とする", ()->{
// letでテスト対象のを作成することができる
Supplier<StockWatcher> stockWatcher = let(() -> {
StockWatcher w = new StockWatcher();
w.setThreshold(1500);
return w;
});
describe("株価が500円の場合", ()->{
// itがテストケースに相当する
it("アラートの状態はOFFである", () -> {
assertFalse(stockWatcher.get().getAlert(500));
});
});
describe("株価が1600円の場合", ()->{
it("アラートの状態はONである", () -> {
assertTrue(stockWatcher.get().getAlert(1600));
});
});
});
});
}}
Gherkin形式のテスト
Gherkin形式でテストを書くと、以下のように書けます。
import static org.junit.Assert.*;
import static com.greghaskins.spectrum.dsl.gherkin.Gherkin.*; // Sectrum固有のもの
import org.junit.runner.RunWith;
import com.greghaskins.spectrum.Spectrum; // Sectrum固有のもの
import com.greghaskins.spectrum.Variable;
/**
* Gherkin形式のテスト
* feature, scenario, given, when, then形式でテストを記述できる
*/
@RunWith(Spectrum.class)
public class StockWatcherGherkinTest {{
feature("株価アラートのテスト。", () -> {
scenario("与えられた株価がしきい値を超えているかどうかを判断するテスト。", () -> {
StockWatcher w = new StockWatcher();
final Variable<Boolean> alert = new Variable<>();
given("株価のしきい値が1500円とする", () -> {
w.setThreshold(1500);
});
when("株価が500円の場合", () -> {
alert.set(w.getAlert(500));
});
then("アラートの状態はOFFである", () -> {
assertFalse(alert.get());
});
});
scenario("与えられた株価がしきい値を超えているかどうかを判断するテスト。", () -> {
StockWatcher w = new StockWatcher();
final Variable<Boolean> alert = new Variable<>();
given("株価のしきい値が1500円とする", () -> {
w.setThreshold(1500);
});
when("株価が1600円の場合", () -> {
alert.set(w.getAlert(1600));
});
then("アラートの状態はOFFである", () -> {
assertTrue(alert.get());
});
});
});
}}
従来の(JUit)のテスト
従来の書き方だと、以下のように書けます。
import static org.junit.Assert.*;
import org.junit.Test;
/**
* 従来のJUnit形式のテスト
*/
public class StockWatcherJunitTest {
@Test
public void 株価のしきい値が1500円の場合_500円の株価に対してアラートの状態はOFFである() {
StockWatcher w = new StockWatcher();
w.setThreshold(1500);
assertFalse(w.getAlert(500));
}
@Test
public void 株価のしきい値が1500円の場合_1600円の株価に対してアラートの状態はONである() {
StockWatcher w = new StockWatcher();
w.setThreshold(1500);
assertTrue(w.getAlert(1600));
}
}
テストレポート
Gradleからテストレポートを生成すると、以下のようなものが生成されました。
Spec形式のケースはitと対になったdescribeのみテストクラスとして扱われるようです。Gherkin形式のケースはシナリオ単位でテストクラスとして扱われます。
所感
良いところ
従来の(JUnitの)仕組みを変えずに、テストの書き方だけBDDのスタイルに変更できる。部分的に導入して徐々にBDDを取り入れる事ができる。
残念なところ
せっかく仕様を日本語の文章で記述できるのに、レポートがソースコードの文章と対応していない。例えば、入れ子にしたdescribeの外側に書いた文章やfeatureに書いた文章はレポートに出力されない。さらに、文章が長くなるとレポートのリンクからたどれなくなってしまう。原因はdescribeなどに書いた文字列がレポートのHTMLファイルのファイル名に使用され、文章が長いとOSのファイル名の長さの制約を超えてしまう事があるからです。(下図参照)
Gherkin形式においては、given, when, then それぞれがテストケースとして扱われるため、結果の量が増えてしまう。(given, whenは当然成功する。)レポートに出力する場合、テストケースがアルファベットの昇順にソートされて表示されるため、同じシナリオの中で複数のgiven, when, thenを記述すると全てのgivenだけ先に表示され、見づらい。given, when, thenの組み合わせが1つの場合でも、given, then, whenの順番になってしまい、やはり見づらい。JGivenのレポートには遠く及びません。
レポートがソースコードの記述に対応しなければ、開発者以外の関係者がBDDに参加することが難しくなってしまう。
まとめ
Spectrumは従来のJUnitによるテストの仕組みを変えずにBDDスタイルでテストスクリプトを作ることができるフレームワークです。試験的に導入し、徐々にBDDを実践するにはよいと思いました。
ただし、テストレポートの出力がソースコードの記述に完全には対応していないため、実際にBDDを回すのは難しいと思います。