アプリ開発やサイト制作のスマホ端末実機検証・テスト-Remote TestKit

テスト駆動開発の基礎

テストを活用した開発手法の一つ、テスト駆動開発について学習します。ここではテスト駆動開発の基本的な流れや目的、環境について扱います。
テストを活用した開発手法の一つ、テスト駆動開発について学習します。ここではテスト駆動開発の基本的な流れや目的、環境について扱います。

1. テスト駆動開発とは

テスト駆動開発(TDD)とは、テストファーストによって開発を進める開発手法です。アジャイル宣言で有名なケント・ベックが手法としてまとめ生まれたもので、主にアジャイル開発プロセスで普及しています。
テスト駆動開発はテストファーストによる追加・変更と、リファクタリングによる設計改善という、2つの活動で構成されます。継続的にユニットテストを使って設計検討やチェック、リファクタリングを行うことにより、テスタビリティに優れバグの少ないソースコードを実現します。

2. テスト駆動開発の流れ

2.1 テスト駆動開発の基本サイクル

テスト駆動開発は以下のRED、GREEN、REFACTORという3つの作業サイクルを細かく繰り返してプログラミングを進めていきます。

(1) RED

失敗するテストを書く。

(2)GREEN

テストを成功させる最低限のコードを書く。

(3)REFACTOR

コードをリファクタリングしてきれいにする。

なおRED、GREENという言葉は、JUnitなどTDDで多用されるテスティングフレームワークの多くがテスト失敗を赤色表示で、テスト成功を緑色表示で通知することに由来しています。
前述しましたが、このサイクルでは以下の2つの活動を実現します。

  • テストファーストによる追加・変更
    最初に失敗するテストを書き、次にそれを成功させる最低限のコードを書く、というステップを繰り返すことでプログラミングを進めていきます。なおこのテストを書いてコードを書くまでのサイクルは、テスト駆動開発ではしばしば数十秒から数十分の超短期 で実行されます。
  • リファクタリングによる設計改善
    テストファーストでプログラミングを進めているうちにソースコードが粗雑になってきたら、早めにソースコードをリファクタリングしてきれいにします。なおリファクタリングではテストファーストで作成したテストを回帰テストとして活用します。
テスト駆動開発の基本サイクル

テスト駆動開発の基本サイクル

2.2 実際の例

基本的な流れを、うるう年判定関数isLeapYear()の実装を例に説明します。
テスト駆動開発では、コードに手を付ける前にしばしば開発対象の仕様をテストリストとして整理します。

テストリスト:
4で割り切れる年はうるう年と判定する
ただし、100で割り切れる年はうるう年でないと判定する
ただし、400で割り切れる年はうるう年と判定する

テストリストで整理したら、プログラミングに移ります。最初にテストを記述し、それを失敗させます。

TEST(isLeapYearTest, 4で割り切れる年はうるう年と判定する)
{
    EXPECT_EQ(true, isLeapYear(8));
}

なおisLeapYear()は以下のように仮実装しても良いでしょう。

bool isLeapYear(int year)
{
    return false;
}

そしてテストを実行し、テストを失敗させます。テスト失敗を確認したら、次にテスト失敗をテスト成功に変えるように、isLeapYear()を実装します。
例えば以下のように実装します。

bool isLeapYear(int year)
{
    if (year % 4 == 0) {
        return true;
    }
    return false;
}

そしてこのテストを実行させ、失敗していたテストが成功に変わったことを確認します。
テスト成功を確認したら、新たなテストを追加します。

TEST(isLeapYearTest, 4で割り切れる年はうるう年と判定する)
{
    EXPECT_EQ(true, isLeapYear(8));
}

TEST(isLeapYearTest, 100で割り切れる年はうるう年でないと判定する)
{
    EXPECT_EQ(false, isLeapYear(200));
}

これを実行してテスト失敗を確認したら、テスト失敗を解消するようにisLeapYear()を実装します。

bool isLeapYear(int year)
{
    if (year % 4 == 0) {
        if (year % 100 == 0) {
            return false;
        }
        return true;
    }
    return false;
}

そしてテストを実行し、テストが成功していることを確認します。
さて、Red⇒Greenのサイクルを2回まわしてisLeapYear()の実装を進めてきましたが、条件分岐が汚くなってきているため、今度はリファクタリングを行います。リファクタリングではまずテストを実行してテストが成功しているのを確認します。そしてisLeapYear()の記述改善に取り掛かります。例えば以下のように改善します。

bool isLeapYear(int year)
{
    if ((year % 4 == 0) && (year % 100 != 0)) {
        return true;
    }
    return false;
}

リファクタリングが完了したら、もう一度テストを実行して、テストが成功していることを確認します。このようにテストファーストでプログラミングを進めつつ、こまめにリファクタリングを行うプログラミングスタイルを取るのがテスト駆動開発です。

3. テスト駆動開発の目的

テスト駆動開発は開発手法であり、そのテストはプログラミングを支援することを目的に活用されます。主なテスト駆動開発の目的を以下にまとめます。

すばやいフィードバックの確保

自分の作業がきちんと達成できていることを、テストで即時に確認できるようにします。
例えばテストコードを追加したら、テストの失敗をチェックすることでそのテストが動いていることを確認します。プロダクトコードを追加したら、テストコードが失敗から成功に変化することをチェックして、実装が問題なく達成されていることを確認します。リファクタリングを行ったら、テストの結果が変化していないことをチェックすることで、意図しないバグや副作用が混入していないことを確認します。
テスト駆動開発では、こうしたフィードバックを数十秒から数十分の超短期で得ることにより、プログラミングミスを即時に検出できるようにします。なおこのメリットはプログラマに「プログラミングは順調だ」という安心感を与えることから、ケント・ベックや和田卓人氏などテスト駆動開発のエバンジェリストは、しばしばテスト駆動開発を「心を健康にする手法」等と紹介することもあります。

設計改善効果

テスト駆動開発では、テストによるバグや作業ミスの検出効果以外でも、製品コードの設計を改善する効果も持っています。
まずテスト駆動開発は、製品コードのテスタビリティやリファクタリング容易性を改善します。というのも、テスト駆動開発の適用範囲では、そもそもテストが記述可能な製品コードしか実装されません。また頻繁にリファクタリングを行うことでリファクタリング容易性も自ずと組み込まれます。
さらにテスト駆動開発は、テストファーストによる設計・実装の考え方の転換により、テスタビリティやリファクタリング容易性以外の設計品質を改善させる効果も持っています。
例えばテスト駆動開発では、プログラミングにおいて製品コードのふるまい、インターフェース、実装方法を以下のような順序で考えることになります。

  1. 製品コードのふるまいとインターフェースを考える
  2. ユニットテスト上で、製品コードのふるまいとインターフェースを実装する
  3. 製品コードの中身を考え、実装する

こうしたステップを踏むと、実行フローといった細部に注意を奪われる前に「インターフェースは良いだろうか」「実装するふるまいに問題はないだろうか」と検討できるようになります。そしてその結果、適切なカプセル化、保守性に優れたインターフェース、仕様の抜け漏れの検出、といった効果が得られるようになるのです。

ユニットテストの確保

またテスト駆動開発はプログラミングの副産物として、ユニットテストを出力しますが、それらは以下のような効果を合わせ持ちます。

  • 最低限のテスタビリティやリファクタリング容易性を支える回帰テストとして使用できます。それを継続的インテグレーションに組み込むと、テストファーストやリファクタリングの起点となるテスト成功状態を維持しやすくなります。この構成は複数人でのTDDの運用を容易にします。
  • テストコードを拡充することで、網羅的なバグ出しや機能的な保証を行うユニットテストを容易に得ることができます。その労力は、ゼロからテストを作成するよりずっと少なく済みます。
  • TDDで作成したユニットテストは、製品コードの仕様やふるまいをわかりやすく示している場合が多いでしょう。これらはテストコードを一種の仕様書として扱うプラクテ ィスをサポートしています。

ただ注意として、純粋なTDDではユニットテストはプログラミング支援を目的に作成されるため、多くの場合において、網羅的なバグ出しや機能性の保証のためのテストとしてそのまま転用できません。TDDのユニットテストを広く活用したい場合は、テスト設計の補強といったアクティビティを追加する必要があります。

4. テスト駆動開発を支える環境

TDDの運用に当たっては、プログラミングの中で軽快かつスムーズに利用できるユニットテスティングフレームワークが重要です。特に、以下のような特徴を持つ環境が推奨されます。

1.軽快にテストを実行できる

TDDにとって、実行に1秒以上かかるテストはもう遅すぎます。そのような実害ある遅さの直接的原因になるフレームワークは避けた方が無難です。

2.簡潔なテストコードでテストを実装・実行できる

Assertion MethodやTest Methodなどの構文が簡潔で、またテストを実行するためのコードもコンパクトに済ませられるものが推奨されます。

3.明快かつ軽快にテスト結果を通知できる

テスト結果を通知するUIは、REDかGREENかの把握を一瞬で済ませられるように、色や配置等が工夫されていると効率が向上します。

4.製品コードの開発環境との切り替えが容易

TDDでは製品コードとテストコードの開発を頻繁に切り替えていくことになるため、それらの編集領域を簡単に切り替えられる環境が望まれます。例えばIDEに組み込み可能なフレームワークは有望な選択肢です。

なおTDDでの利用を想定したユニットテスト環境はすでに多数存在します。例えばJavaならばIDEに組み込んだJUnit、C++ならコンソールベースのGoogle TestといったものがTDDでしばしば利用されています。

Eclipseに組み込まれたJUnit

Eclipseに組み込まれたJUnit

5. テスト駆動開発をはじめる

テスト駆動開発を実践するにあたっては、次の点に留意するとよいでしょう。

  • テスト駆動開発はプログラミング手法であり、テストファーストのサイクルを動かすテンポやスピード感、ツールの活用ノウハウ(ショートカットキーやIDE機能などの活用ノウハウ)、リファクタリングのタイミングなど、プログラミングの細かい技能の影響を大きく受けます。そのためテスト駆動開発を勉強する際は、書籍やコードだけでなく、熟練者の実例を見て細かな技能を把握しておくことも重要です。
  • テスト駆動開発は絶対的なプログラミング手法ではありません。やりやすいところをテスト駆動開発で開発し、テスト駆動開発が苦手とするところは別の手法で開発するというのも妥当なアプローチです。
    なおテスト駆動開発が苦手なところとしては、テスト実行に制約のあるコンポーネント(たとえばデータベースやUI)に依存するコード、プロトタイピングなどでの抜本的な変更が頻繁に行われるコード、処理に時間のかかるコードなどがあります

なおテスト駆動開発は汎用的なプログラミング手法です。環境が整っていればアジャイル開発であれウォーターフォールであれ、プログラマ一個人が独力で実践できます。テスト駆動開発はどこにでも適用できる最上のプログラミング手法というわけではありませんが、プログラミングに携わるエンジニアならば、安心して開発を進めるための考え方の一つとして身に着けておくとよいでしょう

執筆者プロフィール  goyoki

WACATE実行委員 / TDD研究会 / TDDBC
医療機器の開発やテストに従事。テストと開発のコラボレーションやテストの効率化について検討を行なっているほか、テストに関して講演や執筆活動を続けている。