各振る舞いのくくり方を統一することで理解しやすくなり、プロダクトへの変更やバグが発生した場合の影響や理由を分析しやすくなります。例えば、次のようなものが挙げられます。
分類する方法は、ディレクトリ、ファイル、メソッド、アノテーションなどによるタグ、クラスやメソッドでのネストなどがあります。どれが最適であるかは利用するフレームワークによって異なります。これらをテストレベルや網羅基準によって使い分けることで見通しの良いテストコードになります。
「一つのシナリオにおいて何をどの順番で行うか」によって分類する場合です。このパターンを採用する理由は「意味のない名前を付けてしまう振る舞いを減らす」ことや「重複して変更箇所が大量になるテストコードを減らす」という点です。
おそらく、多くのテストメソッドは書きやすさからこの分類を採用していることが多く、重複したテストメソッドを共通化する場合や、パラメタライズテストを行う場合は「振る舞いの組み合わせ方」でくくっていることが多いでしょう。
例えば、テストコードにおけるプロダクトコードのメソッドや関数をどのように実行するかです。プロダクトコードで言えば「メソッドを共通化しよう」と言われるような状況がよくこれに当てはまります。
「振る舞いを構成する入力や各振る舞い自身」によって分類する場合です。このパターンを採用する理由は「何が変わると振る舞いが変わるのか、変わらないのか」を明確にするという点です。
振る舞いを実現する入力値によって分類する場合の代表例としては、入力となる引数や設定値などを網羅対象として見なし、パラメタライズテストを構築する場合などが該当します。振る舞い自身によって分類する場合の代表例としては、RSpecなどで使えるContextのネストなどはこれに該当します。各シナリオで共通している部分がインデントで分かりやすい見た目になります。
また、前回までのBDDの説明において記述したように、Given/When/Thenは状態遷移と見なすことができるので、振る舞い自身によって分類する場合を状態遷移図のパスを書いていると見なして記述することもできます。この場合には、状態遷移のパスがどの程度網羅されているかの判断材料の1つとして各インデントと、インデントのネスト数が利用できます。
テスティングフレームワークや言語によっては、入力によって分類することが難しい場合もあります。例えば、例外発生に関するテストの中には、別のテストメソッドでくくらなければいけないものもあります。
また、パラメタライズテストなどを活用していく中で、後述する振る舞いの出力と併せて全てを網羅するようなテストメソッドを書いていくことは可能です。振る舞いが小さいものであれば、それは十分に可能で、理解性が向上するのでぜひ試すべきです。しかし、振る舞いが大きくなるほど、現状のテスティングフレームワークでは非常に読み書きしにくいことがほとんどです。複雑で読みにくいシナリオを書くことになり、シナリオにバグを埋め込んでしまう可能性が高くなります。
テスティングフレームワークやライブラリを自作やカスタマイズしない状況では、何らかの形で読み書きしやすい方針を採ることで、ある網羅基準を満たしているかが明確には識別しにくくとも、テストコードへのバグ混入を避ける方が重要である場面が多いと筆者は考えています。
「振る舞いで起きる変化」によって分類する場合です。このパターンを採用する理由は「どのようなことが実現可能なのか」を明確にするという点です。
一つの方法として、比較的大きな単位であるフィーチャ名やクラス名で表現することがあります。実際にはテスティングフレームワークが粒度を規定していることが多いです。出力でまとめる場合には、同じような出力を得るために同じ対象を使ってもさまざまな方法が存在するケースが多く、それが同一の結果になることを書くためには、いくつかの振る舞いをテストコード側に別途、メソッド抽出のような形で定義する必要があるでしょう。
特にインテグレーションテスト以降のテストレベルではシステムの利用者が増えるほど、別の方法で同じことが実現できていること、もしくはできないことについて確認が取りやすいと、システムの変更に対する判断をしやすくなります。ある機能を変更することによる他への影響が分かりやすいためです。
「この振る舞いが必要である理由」によって分類する場合です。このパターンを採用する理由は「保守しなければいけないかどうかを判断しやすくする」「保守しようとしているものが矛盾していないかどうかを判断しやすくする」という点です。
最初に振る舞いを記述するときには、テストレベルによって、つまり対象を使うユーザーごとに「○○を達成するために必要」となります。インテグレーションテストレベルより高い場合には比較的ビジネス寄りの理由になってきますし、コンポーネントテストレベルより低い場合には比較的技術的な理由になってきます。もし、アジャイルでユーザーストーリーを使っている場合には「Why」によって分類するというイメージがほぼ近いです。
また、実装における特定のパターン(バグがありそうなパターン)の振る舞いもありますし、外部的な要因にどこまで影響を受けるのかを明示的にする振る舞いもあります。このようにしておくと、「テストが失敗してしまったが、なぜそれが必要なのか」分からないような「パラメタライズテストにある謎の組み合わせ」で困惑するような事態を防ぎやすくなります。
「その振る舞いを利用するユーザーにおける重要度」で分類する場合です。このパターンを採用する理由は「プロダクトを実装する優先順位を判断しやすくなる」「保守しなければいけないかどうかを判断しやすくなる」「プロダクトのコア部分とそれ以外の違いを意識しやすい」という点です。
ある振る舞いのビジネス的もしくは技術的な重要度が分かることも大切ですし、振る舞いにおける入力値の組み合わせの重要度が分かることも大切な場面が存在します。よくあるのは、特定のフィーチャやメソッドにおいてある入力値の場合には別の結果になったり、外部要因の例外的な振る舞いに対しても安全に振る舞うというケースの重要度を低くしたりすることです。
筆者は、これらについてはアノテーションやパッケージ、プロジェクトを使って分類することが多いです。
例えば、「Must」プロジェクトと「Should」プロジェクトにしておき、ShouldプロジェクトでMustプロジェクトを参照するようにすることで、Mustプロジェクトで定義したユーティリティなどを利用しながらより細かい組み合わせの振る舞いを記述したり、より長い組み合わせを簡単に記述できたりします。
また、こうしておくことで、重要度の低い振る舞いやユーティリティがMustな振る舞いを記述する箇所から除外されるので、プロダクトのコアを理解しやすくなります。
TDD/BDDを採用すると、たくさんのテストコードを書くことになります。そこで今回は、振る舞いとして書いた上で、それらをどのように自分たちのプロダクトとして保守しやすくし、また、どのようにしてより良いフィードバックを得るかについての下記三つのポイントを紹介しました。これらは前提として、保守する人と保守する期間に応じて表現方法を変える必要があります。
プロダクトをどのように知りたいのか、プロダクトからどのようにフィードバックを受けたいのかという視点でTDD/BDDを行うことで、大量のプロダクトコード、テストコードとうまく付き合えるようになり、システムを成長させるスピードをできるだけ維持できます。
次回は、TDD/BDDを行う上で話題になりがちなスタブやモックについて触れる予定です。
ソフトウェアテストアーキテクト。ソフトウェアテストを専門にし、テスト戦略やテスト設計に関するモデリングの手法やアジャイルなソフトウェアテストに関する研究と実践を行っています。GroovyやF#などのプログラミング言語を好んでいて、断然IntelliJ IDEA派です。Nagoya.Testing、SCMBootCamp、基礎勉強会などの勉強会を主催しています。
Copyright © ITmedia, Inc. All Rights Reserved.