TDD/BDDにおいて「振る舞いを記述している」とは、どういったことでしょうか。振る舞いの定義、表現力という切り口で説明し、実際に幾つかのテクニックを紹介します。
前述の「『振る舞い』とは何か」の節の立場では、振る舞いの記述は「対象における何らかのイベントにひも付く外部から見える変化の記述」といえますし、より詳細にいえば「オブジェクトの組み合わせの記述」ともいえるでしょう。
「振る舞いを記述している」とは、(例えば)「オブジェクトの内部については関知せず、オブジェクト間を記述する」ことになります。また『GOOS』では、オブジェクト間で発生するコミュニケーションパターンのことを「ドメインモデル」と呼んでいます。
「オブジェクト間を記述する」ことが、各メソッドに対するテストコードである場合は、「振る舞いを記述している」とはあまりいえません。それは先の「設計が悪い場合/説明的な記述になっていない場合に起こる3つの問題」で挙げた3つの問題のうち、どれかを再現させてしまうでしょう。
つまり、Given/When/Thenを使ったとしても、「振る舞いに対するテスト」ではなく「メソッドに対するテスト」になってしまうことがあります。Given/When/Thenは「単なるテンプレート」といえば、それまでなのです。
例えば、「オブジェクト指向における振る舞いを記述する」ことは、「テスト対象のメソッドの組み合わせ方のテストコード」という意味が強いです。つまり、振る舞いのテストは「対象オブジェクトにおけるメソッドの組み合わせによって外部から見える変化に対するテスト」といえます。
これはある種のプログラミング言語の問題ですが、インターフェースを定義することはできても各APIの関連性は定義できません。SMTPなどのプロトコルでは、「この順序で呼んでください」と定義されていますが、それはプログラミング言語のインターフェース定義ではできません。実際は内部実装によって、そのプロトコルが定義されています。
全てのメソッド間が「関係がある」と主張しているわけでもなく、「あるインターフェースにおけるメソッド間の関係定義をできるだけ要らないようにする」方針もあります。後者については「振る舞いを1メソッドで定義できている」ので、それをさらに組み合わせることは、「少し大きな粒度の振る舞いをテストしている」といえるでしょう。
少し大きな粒度の振る舞いをテストすることが必要かどうか、は状況によります。「テスト対象の責務」と「想定している会話方法の確認」が、テスト対象を知るためには大切であり、「振る舞いをテストすることで、それを達成できるだろう」という考え方です。
つまり、必要なテストかどうかは、「少し大きな粒度の振る舞いをテストしておく方が、テストコードの保守への影響を考慮しても、『テスト対象の責務』がより明確になり、全体としての保守性が向上する」と判断できる状況かどうかによるでしょう。
より説明的に振る舞いを実例として記述することで「今、何が求められているのか」の大きな指針になります。いい換えると、「なぜ、この対象が存在するのか」に対する回答を記述しているテストコードとなります。そして誰がそれを要求しているかは、その対象のユーザー(ステークホルダー)によって変化することは前回の記事で触れました。
「対象の振る舞いを記述する」ことについてソフトウェアから離れて例を出します。例えば、あなたが友人や親や配偶者や子供が普段どのようであるかを説明するときに何と答えるでしょうか? 「歩くことができる」「手を握り返せる」「信号の区別が付く」「笑う」という個々の説明よりは、「晴れている日に一緒に歩いていると、赤信号で止まったときに笑いながら手を強く握り返してくる」の方が、より「どんな人であるか」を表現しています。後者が説明的で実例としての振る舞いを表現しているのに対して、前者はどのような人であるかは想像が難しいのです。
このように、「どんな対象であるか」「対象が何をするのか」についてより言及できているものが「表現力がある」といえます。
TDD/BDDにおいてテストコードとして振る舞いを記述することは、バグを見つけるよりもはるかにテスト対象を明確に定義する方法としての側面が大きいのです。
振る舞いの定義、表現力という切り口から見て、どのようなコードがよろしくなく、どのようなコードがよろしいといえるか、の違いが雰囲気で分かる程度にコードで比較します。
public void is_walk_with_boyFriend(){ def boyFriend = new BoyFriend() def mary = new Mary() mary.walk(with:boyFriend) assert mary is WALKING } public void is_walk_with_boyFriend(){ def boyFriend = new BoyFriend() def mary = new Mary() mary.stop() assert mary is STOPPING }
public void is_fun_when_walking_with_boyFriend(){ def boyFriend = new BoyFriend() def mary = new Mary() mary.walk(with:boyFriend, weather:SUNNY) .stop(because:RED_SIGNAL) assert mary.face is SMILE assert mary.squeezeHand is boyFriend.hand }
上記の例は非常に短く簡素なものなので、「必要十分なテストをどう書くか?」というよりは「振る舞いを書いているかどうか」を理解しやすくするためだけのコードであることに注意してください。
最後に、振る舞いを表現しやすくする具体的で簡素なテクニックを2つほど紹介します(参考『BDD in Action』)。
テストコードのクラス名に「{テスト対象クラス}Test」や「{テスト対象クラス}Spec」が使われることがあります。ここから分かるのは「テスト対象クラスが何であるか?」でしかありません。この「{テスト対象クラス}Test」は「○○機能テスト」と書いているに等しいといえます。そして、これは「テストコード内に現れるSUT(System Under Test)オブジェクトと重複している」ともいえます。
class TDDStepTest { TDDStep sut }
TDD/BDDとしてテストコードを書き始めるとき、つまりテストクラスを作るときは、「まだ機能は存在しない」ので「○○機能テスト」というのは作れないはずなのです。頭の中で「○○機能を作る予定だから、○○機能テストとしよう」としているわけで、それはあまり「BDDらしい」とはいい難いのです。
TDDはテストコードという手段を用いて、分析、設計を行うことが主目的です。そこから派生するテストはあったとしても、最初の一歩つまり、特定テストクラスを作り始めるときは、要求や振る舞いを書くことが重要です。その場面で「○○機能テスト」と書いていては、○○機能を実装することにおいて、あまりにも設計理由や設計意図が不明瞭です。
「{テスト対象クラス}Test」よりも良い表現をする方針としては、例えば「コンテキスト」を表現することです。1つのテクニックは「テストクラス名をWhenで始める」ことで、これから実装しようとしていることがより明確になります。もちろん、Whenから始めなくてもコンテキストを表現することはできます。
class WhenCyclingTDDSteps { TDDStep sut }
TDDStepTestよりはWhenCyclingTDDStepsの方が「これから何を実装しようとしているか」が明確になっています。
テストコードのテストメソッド名に「test{テスト対象メソッド}」が使われることがありますが、ここから分かるのは「テスト対象メソッドが何であるか?」でしかありません。
@Test public void testAddStory(){ // 処理 } @Test public void testSortStories(){ // 処理 }
ここから分かるのは「AddStoryメソッドをテストしている」「SortStoriesメソッドをテストしている」ということでしかありません。TDD/BDDにおいて「『(何かは分からないけれど)テストをした』という宣言は重要ではない」のです。このようなテストメソッド名では、一つ一つのテストメソッドを細かく見ていく必要があります。
プロダクトコードと同じように、テストメソッドも名前が重要です。これは「テスト対象が何であるか?」を宣言していますが、それはプロダクトコードにあるべきです。またテストコードには、「テスト対象が何をするのか?」が書かれているべきです。
また、このテストメソッドからは「オブジェクトのプロトコルについて、あまり説明がない」という見方をすることもできます。
「test{テスト対象メソッド}」よりも良い表現をする方針としては、例えば振る舞いを表現することです。1つのテクニックは「テストメソッド名を『Should』で始める」ことや「テストメソッド名を『Test That』で始める」ことで、これから実装しようとしていることがより明確になります。もちろん、「Should」から始めなくても振る舞いを表現することはできます。
ある種(たいていは、多く)のテスティングフレームワークではテストメソッドから別のテストメソッドを呼ぶことはほとんどないわけですから、非常に長いメソッドがあることによる悪影響はプロダクトコードに比べて少ないです。
@Test public void shoudAddStoryByProductOwner(){ // 処理 } @Test public void test_that_developer_team_sort_stories_when_sprint_planning(){ // 処理 }
TDD/BDDを思想とツールを整理し、振る舞いという言葉の使われ方について幾つか引用しました。それらを「振る舞いをテストしていないという状態」に結び付けました。冒頭で述べた3つ疑問については、まとめると次のようにいえます。
いろいろな場所でいわれている「メソッドをテストしていて、振る舞いをテストしていない」とは、辞書で「単語の説明はしているけれど、熟語やよくある言い回しや間違った用法が書かれていない」といい換えられます。言い回しを追加したり変更するときに、単語の意味しか書かれていないと、整合性を保つのが大変です。
また、「ある人物について個々の動作を独立に説明しているけれど、その人物を特徴付ける一連の行動を説明していない」とも言い換えることができます。これを回避するのに、BDDや「Specification By Example」という考え方が役立ちます。
次回はBDDにおけるレイヤーの捉え方、テストレベルとの関係について触れる予定です。
ソフトウェアテストアーキテクト。ソフトウェアテストを専門にし、テスト戦略やテスト設計に関するモデリングの手法やアジャイルなソフトウェアテストに関する研究と実践を行っています。GroovyやF#などのプログラミング言語を好んでいて、断然IntelliJ IDEA派です。Nagoya.Testing、SCMBootCamp、基礎勉強会などの勉強会を主催しています。
Copyright © ITmedia, Inc. All Rights Reserved.