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

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

導入

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

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

展開

テスト駆動開発とは

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

  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;
  }
}

誤った入力や、想定外の入力があった場合のテスト

誤った引数を与えてみる

メソッドは常に想定される入力を受けるとは限りません。先ほどのHealthApp.pdeのテストコードにわざと誤ったデータを仕込みました。このような誤りは、プログラミング言語の文法としての誤りではないため、コンパイラでで検出できません。しかし、実行すると確かにその箇所でエラーを発生します。

わざとエラーを発生させたところ
画像

実際には、誤った入力があった場合にどんな出力があるべきかをあらかじめ判断し、その予想される結果を返せば合格であるテストコードを書きます。そうしないと、私が紹介しているこの手法では、エラーがあるところで実行が停止してしまいます。

そこで、例えばJava言語のユニットテストツールであるJUnitでは、テストコードが望まれる結果を得なくても、最後までテストを実行します。そして発生するエラーを集計して発生率の百分率を表示してくれます。Processingにもこのようなツールが登場すると良いですね。

こうして、コードを実行して動作に問題がないかを確認します。

想定外の引数を与えてみる

例えば今回のコードで言えば、性別にMALEFEMALEどちらかを必ず入力してくれるとは限りません。この入力を保証するための工夫はいくつもありますが、今回は入力が文字列であることを変えず、正しい入力が得られなかった場合には、その旨を表すデータを保持することとしてみましょう。最初の仕様にはなかったことなので、ここでは適切と思われるコードを考えて実装してみました。MALEでもFEMALEでも無かった場合はNONEがセットされるものとしました。あわせて他の要素についても想定される出力があるか確認しましょう。

次に示すコードがその例です。先ずテストで想定外の性別を入力された場合のテストコードを記述します。そしてそれを満足するようにPersonalData.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.isAthlete()     == true     : "Error";
  // assert pd.getBMI       == 0        : "Error";
  // assert pd.getCategory == 0     : "Error";
  pd = new PersonalData("HOGE",25,80,170,false);
  assert pd.getSex().equals("NONE")     : "Error";
}
想定外の入力に対応するコードを追加したPersonalData.pde(部分)
class PersonalData{

  // (略)
  
  PersonalData(String _sex, int _age, double _weight, double _height,
               boolean _isAthlete) {
    sex = checkSex(_sex);
    // (略)
  }    

  // (略)
  
  private String checkSex(String val){
    if (val.equals("MALE") || val.equals("FEMALE")) {
      sex = val;
    } else {
      sex = "NONE";
    }
    return sex;
  }
}

アルゴリズムのテスト

先ほどのコードでは実装していなかった、BMIと肥満指数の計算コードを実装しましょう。先ずは手計算でどんな結果が出るか予想します。

50kg、165cmの方のBMIは、次のように計算できます。

画像

18.37は低体重のカテゴリ-1に相当します。

PersonalDataクラスのgetBMIメソッドの戻り値が、18.3から18.4の間に入っていれば正しい結果を返しているとしましょう。

[作業]上記のテストを行うコードと、PersonalDataクラスのgetBMIメソッドとgetCategoryメソッドを記述してください。

例として次のHealthApp.pdeのようにコードを書いたとします。変更のある部分のみ示します。

HealthApp.pdeにコードを追加した部分
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.isAthlete()     == true     : "Error";
  assert (18.3 < pd.getBMI()) & (pd.getBMI() < 18.4)   : "Error";
  assert pd.getCategory() == -1         : "Error";
  pd = new PersonalData("HOGE",25,80,170,false);
  assert pd.getSex().equals("NONE")     : "Error";
}

次にPersonalData.pdeへの追加コードを示します。先のテストコードを満足するように書きます。

PersonalData.pdeへの追加コード
  public double getBMI(){
    BMI = your_weight / (your_height * your_height / 100 / 100);
    return BMI;
  }

  public double getCategory(){
    BMI = getBMI();
    if ( BMI < 18.5 ) {
      category = -1;
    } else if ( BMI < 25 ) {
      category = 0;
    } else if ( BMI < 30 ) {
      category = 1;
    } else if ( BMI < 35 ) {
      category = 2;
    } else if ( BMI < 40 ) {
      category = 3;
    } else {
      category = 4;
    }
    return category;
  }

このsketchを実行すると、エラー無く実行されるはずです。エラーがあるならば打ち間違いを探してください。

さて、こうして実装したアルゴリズムは、健康管理アプリとして無事動作するでしょうか。人によってはちゃんと動作しないということでは困ります。そこで、考えられる様々なデータでテストをしましょう。

このgetBMIgetCategoryメソッドは、コンストラクタでセットされた値を元に計算しています。今のところ入力はコンストラクタ以外からはありません。ということで、テストすべき項目は計算式の正しさと、入力されたデータの妥当性を確認すれば良いわけです。それぞれの部分をテストするコードを書いた後、そのテストを合格するようにコードを磨き上げてください。

参考文献『ソフトウェアテスト293の鉄則』に記載の「入力フィールド用のテストマトリクスを作成する方法」では具体的な例が示されていて、とても参考になります。一部を引用します。

入力フィールド用のテストマトリクスを作成する方法

単純な整数フィールドに対しては、どのような入力値をテストすると面白い結果が得られるだろうか。以下に、筆者達がよく使用するテスト項目を披露しよう。

  • なにもしない
  • 空っぽ(デフォルト値をクリア)
  • 桁数や文字数の上限(UB:Upper Bound)を超えたもの
  • 0(ゼロ)
  • 有効な値
  • 上限値
  • 下限値
  • (中略)
  • マイナス値

『ソフトウェアテスト293の鉄則』の第3章「テストの技法」⁠P.52)より

[作業]以下のような条件下でのテストコードをHealthApp.pdeに、それに合格するコードPersonalData.pdeに書きましょう。テストコードは上の『ソフトウエアテスト293の鉄則』からの引用を参考に作成しましょう。

  • コンストラクタに何も引数を与えなかった場合に、デフォルトの値がセットされるようにする。デフォルト値は「男性、17歳、70kg、170cm、アスリートではない」とします。
  • 各値の範囲は、年齢は16歳から110歳、体重は30kgから150kgまで、身長は100cmから200cmまでとします。

次に示すのがテストコードの一部です。演習にてこの作業の要求を満たしてください。

テスト項目の「なにもしない」「空っぽ」については、今回はコンストラクタの呼び出しにおいて引数を与えないことが相当しますから、そのようにしてみます。

「なにもしない」場合に相当する引数無しのオブジェクト生成
  //なにもしない
  pd = new PersonalData();

このようにHealthApp.pdeのメソッドtestPersonalDataに書いて実行すると、"The constructor HealthApp.PersonalData() is undefined"(そのようなコンストラクタは宣言されていません)と怒られます。ですから、引数を持たないコンストラクタを書きましょう。

「なにもしない」場合に相当する引数無しのオブジェクト生成と、そのテストコード
  //なにもしない
  pd = new PersonalData();
  assert pd.getSex().equals("MALE") : "Error";
  assert pd.getAge() == 17          : "Error";
  assert pd.getYourWeight() == 70       : "Error";
  assert pd.getYourHeight() == 170      : "Error";
  assert pd.isAthlete() == false    : "Error";
引数無しコンストラクタ
  //引数無しコンスラクタ
  PersonalData(){
    BMI = getBMI();
    category = getCategory();
  }

残りの要件を満たすためのコーディングは演習とします。

演習

演習1(難易度:mediam)

引数無しコンストラクタを呼んだ場合にデフォルトの値がセットされること、セットされる値の範囲を制限することのコードを完成しましょう。テストコードでは不適正な値がセットされたらデフォルト値がセットされることを確認してください。

まとめ

  • テスト駆動開発とは何かを学びました。
  • テスト駆動開発を用いて小さなアプリケーションを作りはじめました。

学習の確認

それぞれの項目で、Aを選択できなければ、本文や演習にもう一度取り組みましょう。

  1. テスト駆動開発の目的とメリットが理解できましたか?
    1. 理解できた。気持ちよく納得した。
    2. 理解できた。しかし、今ひとつスッキリしない。
    3. 理解できない。
  2. テスト駆動開発を使えるようになりましたか?
    1. 使えるようになった。自分のプログラミングにも活用できそうだ。
    2. 本文の例を理解することはできたが、自分のプログラミングに活用できる気がしない。
    3. 本文の例が理解できない。

参考文献

  • 『ソフトウェアテスト293の鉄則』⁠Cem Kaner, James Bach, Bret Pettichord 著、テスト技術者交流会 翻訳、日経BP社
    • 日本と異なり米国においてソフトウェアテストは1つの独立した「職種」のようです。その世界で長年現場経験を積んだ著者らがまとめた各鉄則は、ソフトウエアを開発する私たちにとって大変有益です。

演習解答

  1. コード例を以下に示します。テストの練習としてテストの項目をごく一部に絞っています。ぜひ参考文献を参照して、必要充分なテストを記述し実行してみてください。

おすすめ記事

記事・ニュース一覧