Processingで学ぶ 実践的プログラミング専門課程

第21回 テスト駆動開発(1) まずテストを書こう

この記事を読むのに必要な時間:およそ 6 分

導入

ソフトウェア開発を専門とするカリキュラムで学んでいるのでない限り,ソフトウエアテストについて学ぶことは無いでしょう。ですからやがて,テストを書かずにコーディングすることが習慣となってしまいます。すると,小さくとも実用的なソフトウエアを作成する際に大変困ることになります。コードを変更・追加した後,ソフトウエアの挙動が意図しないものだった時,その原因を突き止めるために余分な時間を要するからです。この余分な時間を極力発生させず,ソフトウェアの機能を目的の状態へ最短で到達させる方法が,テスト駆動開発です。

テスト駆動開発は,どのレベルの,どんな目的のソフトウェア開発にとっても有益な技術です。様々な方法やツールがあります。この連載では,ツールに頼らない方法を紹介します。

展開

テスト駆動開発とは

これから紹介するテスト駆動開発は,次の手順を追ってコードを書くことです。

  1. 作成したいソフトウェアの機能を明確にする。
  2. 目的の機能を,評価可能な状態まで細分化する。
  3. まず評価のためのコードAを書く。このAをテストコードと呼ぶ。
  4. Aが問題なく実行されるように目的のソフトウエアのコードを作成する。

ここで1.や2.の「ソフトウェアの機能を明確にし,細分化する」とは,クラスを設計し,メソッドの引数や戻り値を明確にすることです。

確実に目的の結果を出力するコードを書き,その土台の上に次のコードを積み重ねていくのが仕事といえるコーディングです。 そのために,3.で言及したように,これから書こうとするクラスとメソッドを徹底的に使用したテストコードAを先に書いてしまうのです。このようなテストをユニットテストと呼びます。もちろんこの段階でテストコードはコンパイルも実行もできません。しかし,このAが正しく実行されるようにコーディングをすること(4.)で,プログラマは目的からそれることを避けられます。 さらに,このAを問題なく実行するようなコードが完成し,次の機能のコーディングに入った後も,テストコードBとあわせてAも実行すれば,その後のコーディングの副作用でAが問題を発生しないかどうかが確認できるのです。

大切なのは,目的のコードを書く前に,テストのコードを書くということです。そしてソフトウェアの完成に至るまで継続して繰り返し同じテストが実行されることです。これで目標がはっきりします。テストのコードが正しく動作すれば,目的のコードが完成したと判定できます。これがテスト駆動開発のメリットです。メソッドごとにテストをすることで,アプリケーションが完成するまでメソッドの実行結果が分からない,という状況を防げます。アプリケーション全体が完成していなくても,コードが動く「実感」を得ながらコーディングできるため,テスト駆動開発はプログラマの精神衛生上大変よろしいのです。

ユニットテストを積み重ねて行くテスト駆動開発は,今や常識と言っても良いほどなのですが,プログラミングの入門者はなかなかこれを実践するチャンスを得ることができません。通常のプログラミング入門書には掲載されていないからです。しかしながら,早い段階でこのテスト駆動開発の習慣を身につけることをお勧めします。

テスト駆動開発の例

概略設計

次のようなソフトウェアをテスト駆動開発してみましょう。

健康管理アプリを作ろう。
個人情報を入力すると、BMIや肥満度を教えてくれる。

まずはこんな大雑把なところからスタートしましょう。 せっかくですから,肥満度が高ければ太った人形を,低ければやせた人形を表示などしてみましょう。男性なら画面を青,女性ならピンクになんて遊びもいいでしょう。このアプリケーションは楽しく使えるほうが良いと思います。自由にイメージを膨らませてコードを加えてください。

このアプリケーションに登場するオブジェクトを次のように考えてみます。

  • ディスプレイウインドウ:人形を表示し,どこかに入力されたデータと,計算したデータを表示する。
  • 個人情報コンテナ:入力されたデータを保持し,そのデータをもとにBMIや肥満度を返す。
  • コントローラ:マウスやキーボードからの入力を受け取り,適切な形式で個人情報コンテナにセットする。またディスプレイウインドウへ必要な情報をセットする。

UMLで表現すると,次のように描けます。

ユースケース図

画像

クラス図

画像

シーケンス図

画像

作成しようとするソフトウェアがかなり具体的になってきましたね。

それでは,必要となるアルゴリズムを調査しましょう。BMIや肥満度はどのように計算すれば良いのでしょうか。WikiPediaの「ボディマス指数」のページを参考にして,次の式と換算表を使用することにしました。

BMIの計算式

画像

BMIと肥満度の対応表

BMI肥満度
18.5未満低体重(やせ型)
18.5以上、25未満普通体重
25以上、30未満肥満(1度)
30以上、35未満肥満(2度)
35以上、40未満肥満(3度)
40以上肥満(4度)

こうして必要な情報を調べてみるとBMIや肥満度に性別や年齢は必要ないのが分かります。そのとき,開発者は立ち止まって考えます。例えば,⁠必要ないので,実装は取りやめようか?」⁠しかし次回作成するUIには,性別を反映したい」⁠ここは今後のアプリケーションの発展の可能性を考慮して,残す方向で進めよう」といったように考えます。このように,ごく近い将来利用する要素であれば,先回りして実装しておくのも許容できます。しかし,利用する予定がないのであれば,きっぱり削除しましょう。必要になってから実装するYAGNI]というXP(エクストリーム・プログラミング)の原則です。

テストコードの作成と,目的のコードの作成

それでは,個人情報コンテナクラスをテストするコードを書いてみます。この段階で個人情報コンテナクラスそのものは,入力を受け付けますが中身は空っぽです。このような状態のコードを「スケルトンコード」と呼びます。当然テストの結果は目的を達成しません。

ユニットテストで使用するのがassert文です。メソッドが目的の値を返すかどうかをチェックする命令です。

私の流儀ですが,テストの場合はテストモードのフラグを立て,テストコードのみが実行されるようにします。アプリケーションとして全体を動作させる場合にはこのフラグを降ろして実行します。こうすると,ユニットテストとアプリケーション全体の動作を機敏に切り替えてプログラミングできます。

はじめに,HealthApp.pdeから書きます。

HealthApp.pde

//HealthApp

boolean DEBUG_MODE = true;

void setup(){
  if (DEBUG_MODE == true){
    println("<<Debug Mode>> setup");
    noLoop();
    size(200,100);
    testPersonalData();
  } else {
    println("<<Run Mode>> setup");
  }
}

void draw(){
  if (DEBUG_MODE == true){
    println("<<Debug Mode>> draw");
  } else {
    println("<<Run Mode>> draw");
  }
}  


void testPersonalData(){
  PersonalData pd = new PersonalData("FEMALE",30, 50,165,true);
  assert pd.getSex().equals("FEMALE")   : "Error";
  assert pd.getAge()        == 30       : "Error";
  assert pd.getYourWeight() == 50       : "Error";
  assert pd.getYourHeight() == 165      : "Error";
  assert pd.isAthlete()     == false    : "Error";
  // assert pd.getBMI       == 0        : "Error";
  // assert pd.getCategory  == 0     : "Error";
}

HealthApp.pdeに書かれたテストコードを満たすように,次のPersonalData.pdeにコードを書きます。スケルトンコードではなく最低限動作する状態のコードを書いています。

PersonalData.pde

class PersonalData{
  String  sex           = "MALE";// 性別 MALE,FEMALE
  int     age           = 17;    // 年齢
  double  your_weight   = 70.0;  // [kg]
  double  your_height   = 170.0; // [cm]
  boolean isAthlete     = false;  // アスリートかどうか
  double  BMI           = 0;     // BMI
  double  category      = 0;     // 肥満指数

  PersonalData(String _sex, int _age, double _weight, double _height,
               boolean _isAthlete) {
    sex = _sex;
    age = _age;
    your_weight = _weight;
    your_height = _height;
    isAthlete   = _isAthlete;
    //BMI = getBMI();
    //category = getCategory();
  }    

  public String getSex(){
    return sex;
  }

  public int getAge(){
    return age;
  }

  public double getYourWeight(){
    return your_weight;
  }

  public double getYourHeight(){
    return your_height;
  }

  public boolean isAthlete(){
    return isAthlete;
  }

  public double getBMI(){
    return BMI;
  }

  public double getCategory(){
    return category;
  }
}

著者プロフィール

平田敦(ひらたあつし)

地方都市の公立工業高等学校教諭。趣味はプログラミングと日本の端っこ踏破旅行。やがては結城浩氏のような仕事をしたいと妄想している。

Twitter : @hirata_atsushi

コメント

コメントの記入