TDD/BDDにおける「振る舞い」の意味するところとは何なのか:いまさら聞けないTDD/BDD超入門(3)(2/3 ページ)
BDD初心者が持ちがちな3大疑問点を提示して、さまざまな角度からそれを明らかにしつつ、振る舞いを表現する2つのテクニックを紹介する。
【疑問2】「振る舞いをテストしていない」とはどういうことか
先に挙げたような「振る舞い以外を記述しているようなテスト」とは、『GOOS』の「振る舞いのテストを行え、メソッドをテストするのではない」という言葉から、「メソッドのテストをしている」ことになります。
また、振る舞いを記述していないテストを言い換えると、「構造のテスト」「仕組みのテスト」「実装のテスト」「メソッドのテスト」といえます。「実装のテストをしている」という指摘には少なくとも2つの場合があります。
- 設計が悪い場合
- 説明的な記述になっていない場合
設計が悪い場合
前者の「設計が悪い場合」とは、例えば「デメテルの掟」(デメテルの法則、Law of Demeter(LoD))を破って内部構造を露出している設計によって、テストコードがその露出している内部構造に依存したコードを書いてしまっている場合です。テストコードで「デメテルの掟」を破っていたら、振る舞いをテストしていることにはなりませんし、再設計をする必要性があるかもしれません。
例えば、次のようなコードです。
@Test public void デメテルの掟を破っているテストコードの例(){ TweetLogService .findUser("John") .getSetting() .setSecretMode(true) } @Test public void デメテルの掟を破らないようにしたテストコードの例(){ TweetLogService.hideTweetLogFromOthers(user:"John") }
説明的な記述になっていない場合
「説明的な記述になっていない場合」とは、「テストコードが実装に対するコメントになっている場合」といえます。各メソッドに対する条件が羅列されたテストがあるだけでは、「対象がどのようにコミュニケーションするのか」を知るのにあまり役立ちません。
「実装のテストをしている」場合に起こる3つの問題
では、これらによってどんなことが起きるのでしょうか? よく聞く問題点として次のようなものがあります。
- 後から見返すとテスト対象が何をしているか分かるが、仕様として正しいのか分からない
- テストが失敗したときにテストが正しいのか、バグが埋め込まれたのか分からない
- 内部的なデータ構造を変更したら、失敗するテストが膨大になってしまう
前2つの問題は、説明的なテストによって対処しやすくなることは前回の記事中におけるDan North氏のブログの引用「表現力のあるテスト名は失敗したときに役に立つ」で取り上げました。
最後は、「(前述した振る舞いの反対語である)『内部構造』に依存したテストであるために起きる問題」といえます。
これらの問題は特に、「プロダクト設計」と強く関わります。先に挙げた『リファクタリング』『レガシーコード改善ガイド』『GOOS』は、「プロダクト設計」と協調して良いソフトウェアにする方法がまとめてある書籍といえます。
TDD/BDDはテストファーストを基軸にすることで、プロダクトコードのある種の品質を向上させることを達成しています。ですが、上記のような振る舞いを表現していないTDD/BDDには悪影響があります。
「振る舞いを表現していないテストファースト」は「テストコードを書くこと」が目的になってしまい、「良いテストによって駆動されて開発がより良くなる」ことが達成できなくなってしまいます。
この種の問題提起をしているブログは幾つか存在しており、各ブログのコメントまで読むと、より理解しやすいです。参考にしてみてください。
- テスト考2014 - Hidden in Plain Sight
- RE テスト考2014 #SWTestAdvent - うさぎ組
- Testing like the TSA by David of Basecamp
このような例をはじめとして、何らかの理由によって保守性の低くなっているテストのことを「Fragile Test」といいます。日本語では「脆いテスト」といわれることが多いです。
脆いテスト(Fragile Test)
『xUnit Test Patterns』にはFragile Testの定義と「なぜ、Fragile Testになっているか」を簡単に診断するYes/Noのフローチャートがあります。
ここでは、Fragile Testの1つの原因である「Behavior Sensitivity」について取り上げます。
Behavior Sensitivity
Behavior Sensitivityとは「プロダクトに機能の追加やバグを修正したら、コンパイルは通っていてインターフェースも変わっていないが、テスト実行時に予期せぬテストが多数失敗してしまう」ときに考えられる理由です。
ただし、「自動テストはリグレッションテストである」という側面もあるので、一概にBehavior Sensitivityであるとはいえません(新しいバグが発生していることを検知した可能性もあります)。
このような状況において、テストの事前準備、検証、事後処理のコードがプロダクトの変更部分に関わっている場合には、「Behavior Sensitivityである」といえるでしょう。
プロダクトコードが変わったことによって、テストが想定している仮定が変わっている以上、テストの更新は避けられない問題です。こういった仮定に対する処理(事前準備、検証、事後処理をするコード)を共通化することで、修正すべきコードを減らすことで対処します。
現状のテストコードが次のようだとします。
@Test public void should_be_RED_when_adding_testcase(){ def sut = new TDDCycle() sut.addTest() sut.runTest() assert sut.status = TDDCycle.RED } @Test public void should_be_GREEN_when_implementing_product_code(){ def sut = new TDDCycle(TDDCycle.RED) sut.implement() sut.runTest() assert sut.status = TDDCycle.GREEN }
ここで、「TDDCycle」をインスタンス化した後に「runTest」を必ず実行してからではないと、「addTest()」「implement()」「refactor()」を呼べないようにします(例外が発生するようにします)。そうすると、コンパイルエラーは起きていませんが、テストを実行すると「should_be_RED_when_adding_testcase」「should_be_GREEN_when_implementing_product_code」の両方を修正しなければいけないことが分かります。
ここで、事前準備を共通化する必要があることが分かります。例えば、次のように修正します。
public TDDCycle startTDDCycle(status = TDDCycle.INITIAL){ sut = new TDDCycle(status) sut.runTest() sut } @Test public void should_be_RED_when_adding_testcase(){ def sut = startTDDCycle() sut.addTest() sut.runTest() assert sut.status = TDDCycle.RED } @Test public void should_be_GREEN_when_implementing_product_code(){ def sut = startTDDCycle(TDDCycle.RED) sut.implement() sut.runTest() assert sut.status = TDDCycle.GREEN }
振る舞い以外をテストする方法
先の「『振る舞い』の反対語は何か?」の節の立場でいえば、振る舞い以外のテストは「内部構造、仕組み、実装のテスト」といい換えられます。
そして、これはTDDであるかどうかにかかわらずですが、「振る舞い以外のテストに関しては自動テストはするべきではない」といわれることが多いです。「振る舞いを記述していないとはどういうことか」でも述べたように、テストが内部構造に依存していると、テストがプロダクトコードの変更による影響を受ける箇所は増加してしまいます。
ただ、対象のAPIを使った自動テストをするべきではなくとも、その品質を向上させたい場面は存在します。ここでは、具体的な手段として「レビュー」「静的解析」「privateに対するテスト」を挙げます。また、この章でテストと表現しているのは「テスト対象を動作させずに確認するものは『静的テスト』と呼び、動作させて確認するものは『動的テスト』と呼ぶ」という「JSTQB(Japan Software Testing Qualifications Board)」の定義に従っています。
レビュー
さまざまな視点で「レビュー」というものが存在しますが、あるテスト対象の実装、つまり内部の設計に対するレビューはさまざまなレベルで行えます。自動テストと違って、プロダクトコードの変化に合わせて厳しく保守する必要があるものは生成されません。レビューの方法自体が非常に軽量なので、ROIが高い方法として認識されています。
振る舞いのテストにおいても、もちろんレビューは有効ですし、「Specification By Example」は「振る舞いのテストをレビューしやすさといかに結び付けるか」という点で有効に働いています。
なお本稿では、「レビュー」「インスペクション」「ウォークスルー」などの違いは意識せずに「レビュー」で統一しています。
静的解析
バグの温床になりそうなコードを機械的に教えてくれる静的解析のツールも非常に効果が大きいです。レビューで機械的な指摘になりがちな項目も、こういったツールに任せることができます。無償/有償のツールが比較的多くの言語で用意されつつありますし、無償のツールではユーザーが独自に「指摘するルール」を書いてツールを拡張できるものが多いです。
最近では、「SonarQube」というツールが各種言語の静的解析ツールのプラットフォームとして人気があります。
privateに対するテスト
privateなフィールドやメソッドに対するテストは、TDD周辺でよく質問に挙がる項目として有名ですが、基本的にはどの回答においても「すべきではない」とされています。多くのプログラミング言語では「リフレクション」などのメタプログラミングをすることで、privateに対するテストを可能にしますが、「保守性の高い設計を目指しての『カプセル化』を破っている」ことや、「リフレクションを使っているコード自体の保守性が低い」ことが問題点に挙げられています。
サポートされているプログラミング言語が少ないですが、「doctest」をはじめとする「プロダクトコード内にドキュメントのように書けるテストコード」であれば、上記の問題を回避しつつテストコードを記述できます(doctest自体はprivateに対するテストのためにあるのではなく、名前の通りドキュメントとしてのテストを目指したツールです)。
ただし、privateに対するテスト自体がROIが高いか低いかは「状況次第」としか言えず、レビューで済ませる方が効果的であることもあるでしょうし、doctestなどでテストすることが効果的であることもあるでしょう。
Copyright © ITmedia, Inc. All Rights Reserved.