Digital Romanticism このページをアンテナに追加 RSSフィード

2009-08-19

BDDの導入 - Dan North

この記事はDan North氏の記事「Introducing BDD」を氏の許可を得て翻訳した公式版("the official translation")です。(原文公開日:2006年9月20日)




私は1つ問題を抱えていました。様々な環境にあるプロジェクトでテスト駆動開発TDD)のようなアジャイルプラクティスを用いたり、あるいは教えていると、いつも同じような混乱や誤解に行き当たったのです。プログラマが知りたいと望むのは、どこから始めれば良いのか、何をテストすれば良いのか、何をテストする必要がないのか、1つのものに対してどの程度テストすれば良いのか、テストをなんと呼べば良いのか、テストが失敗した理由をどう理解すれば良いのか、ということでした。


TDDに深く入り込むほどに、自分の道程が、言われたことをコツコツやれば徐々に上達するようなものではなく、むしろ行き詰まりの連続であると感じました。「誰かが教えてくれていれば!」と何度も考えたことを覚えています。それは「素晴らしい。扉が開かれている!」と考えたことよりも遥かに多いものでした。そこで、一直線に優れたものを生み出し、落とし穴を避けることができるものとしてTDDを紹介することができるに違いないと思い立ったのです。


これに対する私の答えがふるまい駆動開発(BDD:behaviour-driven development)です。これは既に確立されたアジャイルのプラクティスから発展したもので、アジャイルソフトウェア開発になじみがないチームにとって、より利用しやすく効果的になるように設計されています。さらに時間をかけて、BDDはアジャイル分析と受入テストの自動化といったより広い構図を包括するようになりました。


テストメソッド名は文章にせよ

最初に「なるほど!」と思ったのは、一見シンプルに見えるユーティリティを見せられている時でした。これはagiledoxと呼ばれているもので、同僚のChris Stevensonが書いたものでした。このユーティリティはJUnitのテストクラスを使っていたのですが、メソッド名が普通の文章のように出力されていたのです。あるテストケースを例に挙げます。

public class CustomerLookupTest extends TestCase {
    testFindsCustomerById() { ... } 
    testFailsForDuplicateCustomers() { ... } 
    ... 
}

これが次のように表示されます。

CustomerLookup - finds customer by id - fails for duplicate customers

「test」という単語がクラス名からもメソッド名からも取り除かれ、キャメル書式のメソッド名が通常のテキストに変換されています。やっていることはそれだけですが、効果は驚くべきものです。


開発者たちはこれが、少なくともいくつかのドキュメントには役立つことに気がつき、本当の文章になっているテストメソッドを書き始めました。それだけではなく、ビジネスドメインの言語でメソッドを書くと、生成されたドキュメントがビジネスユーザやアナリスト、テスタも理解できるものになることも分かってきたのです。


シンプルな文章テンプレートによってテストメソッドに集中し続けられる。

この時に、テストメソッドは「should」という単語で始めなければならないという規約が浮かびました。この文章テンプレート、すなわちそのクラスが何かをするべきだという文章テンプレートが意味しているのは、現在対象となっているクラスに対するテストしか定義できないということです。これによって集中し続けることができるのです。名称がこのテンプレートに合わないテストを書いていることに気がついたら、それはそのふるまい("behaviour")が属するのはどこか別の場所だということを示しています。


例を挙げましょう。私は画面からの入力に対してバリデーションを行うテストを書いていました。ほとんどの入力フィールドはクライアントの普通の詳細情報、つまり苗字、名前などでしたが、そこに誕生日と年齢の欄がありました。私は ClientDetailsValidatorTestクラスを書き始め、 testShouldFailForMissingSurnametestShouldFailForMissingTitle といったメソッドを実装していきました。


年齢の計算に入った所で厄介なビジネスルールの世界に入りました。年齢と誕生日が両方入力されていて、整合していなかったらどうするのか?誕生日が今日だったら?誕生日しか入力されていなかったらどうやって年齢を計算するのか?これらのふるまいを記述するメソッド名が徐々に複雑になっていったので、何か別のものに委譲することを考えました。それで AgeCalculator という新しいクラスを導入し、 AgeCalculatorTest を作ることにしたのです。年齢計算のふるまいは計算クラスに移され、バリデータは年齢計算に関して、計算クラスとの相互作用が適切に行われていることを確認するテストが1つあれば良くなったのです。


あるクラスが2つ以上のことをしている場合、私はこれを、他のクラスを導入し、何らかの処理をさせるべきであることの指標と考えます。新しいサービスを、何を行うのかを記述するインターフェイスとして定義し、このサービスをコンストラクタを通じて元のクラスに渡します。

public class ClientDetailsValidator {

    private final AgeCalculator ageCalc;

    public ClientDetailsValidator(AgeCalculator ageCalc) {
        this.ageCalc = ageCalc;
    }
}

このようにしてオブジェクト同士をつなぐ方法は、依存性注入("dependency injection")と呼ばれており、モックと組み合わせると特に有効です。


表現力のあるテスト名は失敗した時に役に立つ

しばらくして、コードを変更してテストを失敗させた場合に、テストメソッド名をみて意図されたコードのふるまいが何かを識別できるということが分かりました。典型的には以下の3つのうちどれかが起こっていました。

  • バグを埋め込んでしまった。悪い子だ。解決法:バグを直す
  • 意図されたふるまいは変わらず関連性があったが、どこか別の場所に移されていた。解決法:テストを移動し、場合によっては変更する。
  • ふるまいがもはや正しくない。システムの前提が変わってしまっている。解決法:テストを削除する

最後の1つはアジャイルプロジェクトにおいて理解が深まるのにあわせて起こりやすいものです。不幸にもTDDの初心者はテストを削除することに対して先天的な恐怖を抱きます。これによって何らかコードの品質が下がってしまうかのように感じるのです。


should」という単語が持つより繊細な性質は、もっと正式な表現である「will」や「shall」と比較した場合に明らかになります。「should」という単語によってテストの前提を問い直すことができます。「そうしなければいけないのか?本当に?」と。このことによって、テストが失敗した理由が自分のバグのせいなのか、システムに関する以前の想定が今では違ってしまっているだけなのかを判断しやすくなるのです。


「ふるまい("behaviour")」は「テスト("test")」よりも使い勝手のいい単語である

今や"test"という単語を取り除いてくれるagiledoxというツールと、各テストメソッド名のためのテンプレートが手に入りました。そこで突然気がついたのが、TDDに関する誤解が「テスト」という単語にまでさかのぼれるということでした。


テストがTDDにとって本質的ではないということを言いたいのではありません。メソッドの結果セットはコードが正しく機能していることを確かめる効果的な方法です。しかし、もしメソッドがシステムのふるまいを包括的に記述していなければどうでしょう。これは間違った安心感を生み出してしまいます。


そこで、TDDを扱う時に「テスト("test")」の代わりに「ふるまい("behaviour")」を用い始めました。そこで分かったことなのですが、これは単にぴったりとマッチするだけでなく、あらゆる種類のコーチングクエッションが一瞬にして解決したのです。TDDに関するこういった疑問のいくつかに対しては、ここで答えることができます。テストをなんと呼ぶべきかという質問の答えは簡単です。これは関心対象となっている、次に実装するふるまいを記述した文章です。どのくらいテストをしたらよいのか、という疑問は意味がありません。1つの文章で記述できるふるまいには限度があるからです。テストが失敗したときには前述したプロセスを経てください。バグを埋め込んでしまったか、ふるまいの場所が変わったか、テストがもはや整合性がとれないものになったか、いずれかです。


テストについて考えることからふるまいについて考えることへのシフトがきわめて深いものであることが分かったので、TDDに言及する時には、BDDか、ふるまい駆動開発と言い始めました。


JBehaveはテストよりもふるまいを強調する

2003年の終わりに、そろそろ発言するだけではなく、お金か、少なくとも時間を費やす時が来ていると判断しました。JUnitに代わるものを作り始め、それをJBehaveと呼びました。ここではテストに関するあらゆる言及が、ふるまいの検証に関連する語彙に置き換えられています。新しくできたふるまい駆動の方針に強く固執したら、このようなフレームワークがどのように発展するのかを確かめるためにこうしたのです。同時にこれは、TDDとBDDについて紹介する際に、テストをベースとした語彙がもつデメリットを避けることができる良い教材になるとも考えていました。


CustomerLookup という仮のクラスに対するふるまいを定義するためにはふるまいクラスを書きます。これを例えば CustomerLookupBehaviour と呼びます。このクラスには"should"という単語で始まるメソッドが含まれます。ふるまいの実行クラスはふるまいクラスをインスタンス化し、各ふるまいメソッドを順次呼び出します。これはJUnitがテストに対して行うのと同じです。実行に合わせて進捗をレポートし、最後にサマリーを出力します。


最初のマイルストーンはJBehaveを自己検証可能にするというものでした。私が付け加えたふるまいは自分自身を実行できるようにするというものだけでした。JUnitのテストをすべてJBhaveのふるまいに移植し、JUnitと同じようにすぐにフィードバックが得られるようにすることができたのです。


次に実装すべき最も重要なふるまいを決定する

次に見つけたのが、ビジネス的価値という概念です。もちろん、自分がソフトウェアを作っているのがある理由によるものだということは常に意識してきました。しかし、ちょうど自分が書いているコードの価値について本当に考えたことはありませんでした。もう一人の同僚であるビジネスアナリストのChris Mattsのおかげで、ふるまい駆動開発の文脈におけるビジネス的価値について考えられるようになったのです。


JBehaveにセルフホスティング機能をつけるという目標を持っていたことで気がついたのは、集中し続ける上で本当に役立つのは次の問いを立てることだということです。システムがやっていないことの中で、次に実装すべき最も重要なことは何か?


この問いに答えるにはまだ実装されていない機能の価値を識別し、優先順位をつけなければなりません。これは同時に、ふるまいメソッドの名前を考えるにも役立ちます。「システムはXをやっていない(Xには何か意味のあるふるまいが入る)、そしてXは重要である。これはXをやらなければいけないということを意味する。」ここから次のふるまいメソッドは単純にこうなります。

public void shouldDoX() { // ... }

ここでもう一つのTDDに対する質問、つまりどこから始めるのかという問いに対する答えを得ました。


要件もふるまいである

ここで私が持っていたのは、TDDがどのように機能するのかを理解し、さらに説明するのに役立つフレームワークと、かつて遭遇した落とし穴を全て避けられるアプローチでした。


2004年の終わりに私が、新しい基盤であるふるまいを基礎とした語彙でMattsに説明していた時、彼はこう言いました。「でも、それは分析そっくりだね。」このことを整理するのに長い時間がかかりましたが、その結果、このふるまい駆動の考えを要件定義に適用することを決めました。アナリスト、テスタ、開発者、そしてビジネスのために統一的な語彙を開発することができたなら、技術側の人間がビジネス側の人間と話をする時に起こる曖昧さや誤解をいくつか取り除く方向に進んでいけるはずです。


BDDは分析のための「ユビキタスランゲージ」をもたらす

この頃、Eric Evansが彼にとってのベストセラーであるDomain-Driven Designを出版しました。その中でEvansはビジネスドメインに基礎をおいたユビキタスランゲージを用いてシステムのモデリングを行うという考え方について説明しています。こうすることでビジネスの語彙が正しくコードベースに浸透するのです。


Chrisと私は自分たちが分析プロセス自体に対するユビキタスランゲージを定義しようとしているのだと認識しました。出発点にちょうどいいものがありました。企業内で一般的に使われているものとして次のようなストーリーテンプレートがあったのです。


As a [X]

I want [Y]

so that [Z]


Yにはなんらかの機能が入り、Zに入るのはその機能がもたらす利益ないし価値です。そしてXは利益を享受する人(もしくは役割)です。このテンプレートの強みは、最初に定義する時にストーリーを実現させることの価値を識別するように強いる点にあります。ストーリーにまつわる実際のビジネス的価値がなければ、次のように落ちていきます。「. . . [ある機能]が欲しい("I want")。 だから("so that") [単にそれをやる。よいだろうか?]」こうすることで、なんとなく付け加えられてしまった不要な要件をいくつか削るのが容易になります。


ここからMattsと私はアジャイルを実践するテスタであれば誰でも既に知っていることの良さを理解する作業に取りかかりました。ストーリーのふるまいはシンプルに言えばストーリーの受入基準("acceptance criteria")です。つまり、もしシステムが全ての受入基準を満たしていれば、システムは正しくふるまっていることになりますし、そうでなければ、システムが正しくふるまっていないということになります。そこで私たちはストーリーの受入基準をとらえるためのテンプレートを作りました。


このテンプレートはアナリストがわざとらしいと感じたり、窮屈に感じたりしないように自由度を高くする必要がありましたが、ストーリーをそれを構成する断片に分解して各断片を自動化できるように、構造化されている必要がありました。私たちは受入基準をシナリオという用語で説明し始めました。これは次のような形をとります。


Given:最初の文脈(前提)があって、

When:イベントが発生した場合、

then:なんらかのアウトプットを保証する。


説明のために伝統的なATMの例を用いましょう。ストーリーカードの1つは次のようになります。


タイトル:顧客が現金を引き出す

As a:顧客の立場で、

I want:ATMから現金を引き出したい、

so that:銀行で並ばなくていいように。


このストーリーがいつ実現したのか、どうすれば分かるでしょうか?考えるべきシナリオはいくつかあります。口座に残高があるかもしれませんし、既に引き出されていても、借り越しの範囲内かもしれません。もちろん、他にもシナリオはあるでしょう。たとえば口座に残高はあるけれど、引き出したことで借り越しになる場合や、ATMに入っている現金が足りない場合などです。


given-when-thenテンプレートを使うと、最初の2つのシナリオは次のようになります。


シナリオ1:口座に現金が入っている

Given:口座に残高があって

And:カードが正しいものであり

And:ATMに現金が入っていれば

When:顧客が現金を要求した場合に

Then:口座に対して請求が行われることを保証する

And:現金が引き出されることを保証する

And:カードが返却されることを保証する


"and"を使って、複数の前提条件や複数のアウトプットを自然につなげていることを付記しておきます。


シナリオ2:口座が借り越し限度を超えて引き出された

Given:口座から過度な引き出しがあって

And:カードが正しいものであれば

When:顧客が現金を要求した場合に

Then:拒否を示すメッセージが表示されることを保証する

And:現金が引き出されないことを保証する

And:カードが返却されることを保証する


どちらのシナリオも同じイベントに基づいており、いくつかの前提とアウトプットを共有しています。前提、イベント、アウトプットを再利用することでこれを活かしたいと思います。


受入基準は実行可能であるべきだ

シナリオの断片、つまり前提、イベント、アウトプットはコードで直接表現することができる位の適切な粒度になっています。JBehaveが定義するオブジェクトモデルによってシナリオの断片をJavaクラスに直接マッピングさせることができます。


各前提を表すクラスを作ります。

public class AccountIsInCredit implements Given {
    public void setup(World world) { ... } 
}
public class CardIsValid implements Given {
    public void setup(World world) { ... } 
}

イベントを表すクラスも作ります。

public class CustomerRequestsCash implements Event {
    public void occurIn(World world) { ... }
}

同じようにアウトプットについてもクラスを作ります。そこでJBehaveはこれらのクラスをすべて結びつけて実行します。まずオブジェクトを保持する"world"オブジェクトが作られ、各前提に順番に渡されることで分かっている状態("state")が設定されていきます。JBehaveは次にイベントに対して、その世界で「起こる("occur in")」よう通知します。このイベントがシナリオの実際のふるまいを実行するのです。最後にストーリーに対して定義した全てのアウトプットに処理が任されます。


あるクラスが各断片を表すようにしたことで、他のシナリオやストーリーで断片を再利用することができるようになりました。最初のうちは、これらの断片はモックを使って実装され、口座に残高があったり、カードが正しいものであるように設定されます。これらがふるまいを実装するスタート地点となるのです。アプリケーションの実装が進むにつれて、前提やアウトプットは実装が済んだ実際のクラスを使うように変更されます。そうしてシナリオが完成する時には、適切で端から端まで一貫した機能テストとなるのです。


BDDの現在と未来

若干中断することもありましたが、JBehaveは実際の開発で使われるようになりました。核となる部分は完成しており堅固です。次のステップはIntelliJ IDEAEclipseといったよく使われているJavaのIDEと統合することです。


Dave Astelsは積極的にBDDを推進してきました。彼のブログとさまざまな記事は立て続けに様々な活動を引き起こしてきましたが、中でも特筆すべきはRuby言語においてBDDフレームワークを作り出すrspecプロジェクトです。


私の同僚の多くがBDDのテクニックを様々な現実世界のプロジェクトで用いてきて、成功をもたらすものであることが分かっています。JBehaveのストーリーランナは受入基準を検証する箇所ですが、これは実際のプロジェクトで利用されています。


構想としては相互変換が可能なエディタを作るというものがあります。これはビジネスアナリストとテスタが普通のテキストエディタでストーリーをとらえ、そこからふるまいクラスのためのスタブをビジネスドメインの言語で生成することを可能にするものです。BDDは多くの人々に助けられて進化しているのであり、私は彼ら全員に心から感謝しています。


この記事が最初に掲載されたのは2006年3月のBetter Software magazineです。




日本語版の公開にあたって、Dan North氏から読者の方へメッセージを頂きました。Thank you, Dan!

I am grateful to you for your interest in Behaviour-Driven Development. I hope you find this article useful, and that you enjoy your journey into BDD as much as I have.

投稿したコメントは管理者が承認するまで公開されません。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証



Copyright(C) 2005-2011 Digital Romanticism. All Rights Reserved.