BDD のすすめ
はじめに
みなさん、テスト書いてますか?
テストって大変ですよね。適切なテストを適切なテストレイヤーに、実装にかかるコストと、実行にかかるコストを鑑みて、配置しないといけません。ユニットテストで実装すべき境界値テストを E2E で実装するのはおかしいのは理解できるとは思いますが、あくまで簡単な例であり、実際のところどちらで実装すべきか曖昧なテストシナリオなんてありふれています。
ここでは、ユニットテストではなく、IT (統合テスト) や E2E に足を伸ばしてみないか? という BDD のすすめと題した記事を書かせてもらいます。本来 IT や E2E はテストの足を引っ張りがちなのですが、ちゃんと実装することにより、強い武器いもなることを知って欲しいと思います。
テストピラミッド
テストピラミットとは、テストにおけるレイヤーとその理想像について端的に表現した図です。 E2E テストや IT テストはユニットテストに比べれば実行が不安定で、実行時間もかかることから抑えた数にすべき、という話です。
これは非常に正しくて、ユニットテストを優先的に実装すべきなのはそうです。とはいえ、E2E と IT の数を意識的に削っている場合もあり、UI のテストにテストカバレッジという概念があるかどうかは分からないですが、意図的にテストカバレッジを下げているということになります。例えば例として、会員登録のフォームとして、氏名、かな、メールアドレスがある時に各フォームに対してバリデーションがかかっており、バリデーションでエラーになった場合、対応するフォームの下に警告として赤文字でエラー内容が表示されるという UI があったとします。この際に、各フォームで警告が表示されることを確認する E2E フロントテストを実行するか? という話しです。全部確認したいところですが、E2E の実行が遅かったり、メンテコストが高かったりすると諦めざるを得ません。
課題
自分は長らく E2E や IT が少ない、またはほぼないと言って良いぐらいのプロジェクトでコードを書いてきました。最終的にできた成果に対してがっつり手で QA を行うという形で品質を担保していて、QA のマンパワーだったり、熟練のスキルでなんとか下支えができているという感じでした。
こういう状況下で一番困るのが再帰テストです。大きな更新がある度に、以前のテストケースからエンバグしてそうなテストケースを探してきて、エンバグしていないかを確認する。規模が大きくなればなるほど再帰テストが厳しくなっていくというジレンマがあります。もちろんこういう時のための E2E、IT テストなのですが、ちゃんと整備されていないと無いのと同じです。
E2E、IT を整備するには幾つかの課題があります。その一つとして実装が長大になってしまいメンテナンスコストがかかってしまうこと、また単純にテストケース自体をメンテナンスしていくコストの重さがあります。(その他にも色々ありますが、今回焦点を当てたいのがこれらの課題です。)
理想
ここで提案したいのが、BDD によるテスト設計とテスト実装を分離した E2E テストの実装手法です。これにより、ユニットテストも大事だけど、ちゃんと E2E でカバーされている範囲も広くて、理想とする総合的な安心感のあるリリースに近づくことができると考えています。テストプラミッドって、先が尖ってるけど、それはそうじゃなくて、ちゃんと台形にして上の方のテストも充実させるのが本来の理想形だぞ、というお話です。
BDD
BDD とは、振る舞い駆動テストのことを指します。端的に BDD を表現すると、システムの振る舞いに焦点を当てた TDD(テスト駆動開発)と言えます。システムの振る舞いに焦点を起き、サービスとしてあるべき動作を確認するということに視点を置いたテストと言えます。(ここについて丁寧に書こうと思うと大変なので、ざっくり書かせてもらいます。 BDD については様々な記事があるので、調べてみて下さい)
ここで『テスト設計とテスト実装を分離した BDD 実装』を紹介しようと思うのですが、まず初めに簡単な TODO 管理サービスについて考えます。この時 BDD におけるテスト設計を以下のような形で記述するとします。ここではシナリオベースでの E2E について考えます。
## TODO を追加すると一覧画面に表示されることを確認する
* ユーザー"A"でログイン
* TODO に"買い物をする"を追加
* TODO 一覧に"買い物をする"があるかを確認
このテスト設計は、まずどのようなテストを行うのか? というテストのお題目と、その確認手段が順番に列挙されています。一般的な E2E テスト実装において、QA のテスト設計者がこのようなシナリオを用意して、それを一つ一つ実装していくという形をとっているところも多いのではないかとは思います。
『テスト設計とテスト実装を分離した BDD 実装』(以下、単に BDD 実装とします)において、よく採用されるのが各テストにおける、確認手段のステップを、ステップごとに関数化してテスト実装をパーツ化していきます。(コードはイメージです)
func ログイン(user string) {
...
}
func TODOに項目を追加(user string, item string) {
...
}
func TODOの項目を確認(user string, item string, count int) {
...
}
そのような実装をすると、先程の E2E テストはコード的にはそれらの関数だけで構成できるようになります。見ての通り、ほぼテスト設計書をそのままコードに落とし込むことができていると思います。
func TODOを追加すると一覧画面に表示されることを確認する() {
ログイン("A")
TODOに項目を追加("A", "買い物をする")
TODOの項目を確認("A", "買い物をする", 1)
}
この手法の利点は、E2E テスト自体の実装を操作単位のパーツで分けることにより、実装自体の手間を減らすことが目的になっています。このサービスは TODO を扱うサービスなので、テストシナリオに TODO に項目を追加するケースは非常に多いと思います。パーツ化していれば類似シナリオにそのまま関数を用いて実装することができます。
かつ、 このように出来上がった関数をみると、それ自体はほぼ仕様書と同等のものになっており、テストコードが仕様書としての役割を果たしていることが理解できると思います。
ライブラリ
ここまで説明をしたら感の良い人は気付いたかもしれませんが、この仕組みを効率的に実装する BDD フレームワークがあります。有名なところで言えば Cucumber で、自分が好きなところで言えば、Gauge がオススメです。どちらにも共通点として挙げられるのが、自然言語で書かれたテスト設計書がそのまま E2E テストとして実行可能になる点です。
ライブラリ実装例
Gauge での実装例について見ていきます。Gauge は他の BDD フレームワークとは異なり、仕様書を Markdown で記述することができます。(Cucumber は Gherkin 記法で記述します)これが仕様書としての役割を果たしやすく非常に便利です。以下に先ほどの TODO アプリでの例を記載します。
# TODO の登録関連のテスト
TODO を登録する時の挙動や、登録した後の状態についてのテストを記述します。
## TODO を追加すると一覧画面に表示されることを確認する
- ユーザー"A"でログイン
- TODO に"買い物をする"を追加
- TODO 一覧に"買い物をする"があるかを確認
見ての通り、一番初めにテスト設計として記述したものがほぼそのまま使用できます。また見出しとリストとリストに紐づくテーブル以外はフレームワークに読み飛ばされ、単純なドキュメントとして記述することができます。これが Gauge におけるテスト設計書になります。これがこのまま実行できると言ったのは、対応するコードの紐付けをフレームワークがサポートしていることにあります。(自然言語とコードの紐付けは BDD フレームワークには基本的に備わっています)
class TodoSteps {
@Step("ユーザー<username>でログイン")
fun stepLogin(username: String) {
// ...
}
@Step("TODO に<item>を追加")
fun stepAddTodoItem(item: String) {
// ...
}
@Step("TODO 一覧に<item>があるかを確認")
fun stepCheckItemInList(item: String) {
// ...
}
}
このように実装して、テストを回すと、Gauge では以下のような HTML でのレポートを出力することができます。テストレポートが仕様書になり、現在どのような進行状況かまでを示す資料として読むことができます。 今回は Gauge (with Kotlin) での例でしたが、Cucumber でも似たような形で実装することができます。
実装例補足
BDD フレームワークでは、各テストの手順を Given
, When
, Then
(こういう条件下で、これをしたら、こうなる) みたいな分類分けをしており、テスト仕様書にもその分類を指定して記載することができるが、殆どのフレームワークでは *
のような単にリスティングする形で、分類せずに各手順を記述する方法がある。(今回の例はそれを用いている) 自分は自然言語であれば、読めば分かるのでこのような分類分けは特に必要無いと考えています。
また、Gauge は BDD フレームワークではないと、制作者サイドは語っています。実際にこれらの仕組みは BDD 以外でも使用することは可能で、E2E テスト以外に使用用途はあります。が、Gauge は BDD フレームワークとして利用されることが殆どなので、今回は Gauge を使った例を出しています。
利点・欠点
BDD の利点として一番あげられるのは、関心の分離で、テスト設計とテスト実装をキレイに分離できることにあります。単純な E2E を実装すると、テスト設計をエンジニアがテスト実装に落とし込みますが、それが正しく実装できているかをレビューしたりなど、テスト設計に関わる人の負荷が減ります。コードの再利用性があがり、ローコストで E2E テストを行えるようになります。既に手順が実装されているものを組み合わせてテストシナリオを作る場合の、エンジニアの工数はゼロです。もちろん分離できていることによってメンテナンス性も高まります。
これにより、より気軽に E2E のテストケースを増やすことが可能になり、テストの実装のコストや、メンテナンスのコストを考慮して設計しなかったテストについても、かなり実装がしやすくなると思います。テストの実行時間については明確な利点は BDD にはありませんが、テストコードが疎になっている分、一部だけ Push 毎に CI で実行して、一部は nightly で実行という形を取りやすいと思います。
欠点として、単純な E2E 実装に比べて、軌道に乗るのが大変というところにあります。自然言語でシナリオを記述し、そこに変数を埋め込む必要があるので (ユーザー"A"でログイン、の"A"をここでは変数と言っています) それを自然な形でテスト設計が書けるまでに落とし込むには、それなりに大変な背景実装が必要になってくると思います。そのため、QA 担当者のみでシナリオ設計してテストを回すというところまで持っていくのに、それなりのコストがかかります。一方で軌道に乗ってしまえば、利点で話した通り、ローコストでシナリオ追加が可能になり、リターンが得られると自分は考えています。
まとめ
自分が何かを作る時には、かなり積極的にこの BDD の仕組みを取り入れています。それにより、例えば、アプリの根幹のフレームワークがメジャーアップデートをした時など、アプリのどこに影響が出てくるか分かりにくく、回帰テストでしかテストできないケースなどに、BDD を利用したしっかりとした E2E が存在することが、大きな安心材料になります。
振る舞いテストである BDD は確かに導入こそ大変な部分はありますが、QA の観点で言うとこれ以上にない武器になると考えています。是非みなさんも試してみて下さい!!!
Discussion