« [XP SP3] リモートデスクトップのクライアントがバージョンアップしてたんですねぇ | トップページ | USB 3.0 ではコネクタ形状が変わる »

2009年6月 8日 (月)

[プログラム設計事始] メソッドの外部設計とテストファースト(1)

前回までで、 メソッドの外部設計を表の形でまとめることを説明してきました。
さて、 表以外に、 メソッドの外部設計を書き表す方法には、 どんなものがあるでしょう。 いろいろ考えられると思います。 コードで記述してみるのは、 どうでしょう?

プログラミング言語を使ってメソッドの外部設計を表現できれば、 次のようなメリットが得られるでしょう。

【 メリット 】

厳密な記述
自然言語での記述は、 どうしても読み手によって解釈がブレる可能性があります。 コードならば、 ( 同じコンパイラを同じように使う限り ) 解釈は必ず一通りになります。

自動的な検証が可能
メソッドの外部設計は、 外から見たメソッドの振る舞いを記述するわけです。
コードで記述するならば、 対象とするメソッドを外から見た振る舞いが設計通りかどうかを、 自動的に検証できるはずです。

そんなことが実際にできるのか、 やってみましょう。
[プログラム設計事始] メソッドの外部設計(1) 」 の最初の設計の表の 3行目を例にとってみます。

記述対象: メソッド string BuildMessage(string targetName)
外部設計(3番目):
 入力 = "{foo}" (1文字以上)
 出力 = "Hello, {foo} !"

これは、 引数 targetName に 1文字以上の適当な文字列 "{なんとか}" を渡してメソッドを呼び出したとき、 戻り値が "Hello, {なんとか} !" になる、 ということでしたよね。
それを C# のコードで書くと、 こんなふうになるでしょう。

string input = "試験";
string output = BuildMessage(input);
if (output == "Hello, 試験 !")
{
  Console.WriteLine("設計通りの振る舞いです");
}
else
{
  Console.WriteLine("設計とは異なる振る舞いをしました");
}

このコードを実行すると、 入力に対する出力が設計通りなら 「設計通りの振る舞いです」 と表示され、 そうでなければ 「設計とは異なる振る舞いをしました」 と表示されます。
このようにすれば、 メソッドの外部設計を、 厳密なコードとして記述することができるわけです。

コードでメソッドの外部設計を書き下すことには、 欠点もあります。

【 デメリット 】

コードを読める人にしか分からない
しかしこれは、 メソッドの外部設計を読まねばならない人はコードを書く人である、 ということから事実上はデメリットにはなりませんね。

一覧性が低い
自然言語を使った表に比べると、 定義が長くなるため、 メソッドの外部設計の全体に対する見通しが悪くなります。 メソッドの外部設計にはすべての入力の組み合わせを書かねばなりませんが、 見落とす可能性が高まります。

後者の問題は、 いろいろと工夫して見落としが減るようにしないといけないでしょう。 それでも、 メソッドの外部設計をまったく書かないよりは、 よほどマシです。

ところで、 上のコードだとせっかく自動的に検証していながら、 検証結果を確認するのは目視です。
結果の確認も自動化できないでしょうか?
メソッドの外部設計のすべてを自動的に検証してくれて、 その検証結果を 「設計通りだったのが xx件 / 設計通りじゃなかったのが xx件 / 詳細は、… 」 といった感じでレポートしてくれると、 検証結果を 1件ずつ目で確認する必要が無くなって、 良いのではないでしょうか。

それを実現したのが xUnit と総称されるツールです。
最初の xUnit は SmallTalk 用に作られた SUnit で、 1990年代の後半に登場しました。
SUnit ( Wikipedia 英語版 )
その後、 Java や .NET Framework など、 多くの言語・プラットフォームに移植されて現在に至っています。 もう 10年以上の実績があるツールなのです。

.NET Framework 用の xUnit は  NUnit です。
NUnit は ( 他の xUnit と同様に )、 検証コードの自動実行・連続実行をサポートし、 検証結果を判定する Assert メソッドを提供しています。 また、 実行する検証コードを選択したり、 検証結果をまとめてレポートする GUI ( テストランナーと呼ぶ ) が提供されています。

xUnit は、 メソッドの外部設計をコードで記述しやすくし、 自動的に検証し、 検証結果をまとめて表示するためのツールです。
しかし、 使い道としては外部設計を表現するだけに限らない ( たとえば、 API の動作検証に良く使います。 また、 あまり勧められる方法ではありませんが、 逆順にして、 書き上げたメソッドの単体テストをするためにも使えます。 ) ので、 xUnit のことを汎用的に 「ユニットテスト フレームワーク」 ( unit-testing framework ) と呼びます。
※ ユニットテストは直訳すると 「単体テスト」 ですが、 xUnit のテスト対象はメソッドであることに注意してください。
※※ 実際には、 Kent Beck 氏が SUnit を考案したときには、 最初からテストファーストのツールとして考えていたらしいです。 ここではメソッドの外部設計書をコードで置き換える、 という流れで説明しましたが、 氏の発想はそうではなく、 テストファーストありき、 だったと思われます。

なお、 NUnit と同等の機能が Visual Studio に取り入れられてきました。 Visual Studio 2008 では Professional Edition 以上のバージョンに組み込まれています。

C# 2008 Express Edition と NUnit 2.5 を組み合わせて使う方法は、 次の記事を参照してください。
・  NUnit 2.5 がリリースされているので、 Windows 7 RC に入れてみた。
NUnit の "Hello, world!" ~ C# 2008 Express + NUnit 2.5 で、 テストファーストの Step by Step

 

ところで。 メソッドの外部設計に限らず、 設計書というものは、 コードを書く前に作ったほうが良いでしょうか、 それともコードを書いてからのほうが良いでしょうか?
いろいろな見方がありますが、 プログラムの品質のためには先に設計書を書くべきでしょう。 そのことに納得できるなら、 メソッドの外部設計書をコードで表現したユニットテストも、 いつ書くべきか分かりますね。
それがすなわち、 「先にテストコードを書け」 ( テストファースト ) ということです。

最後に、 「 [プログラム設計事始] メソッドの外部設計(1) 」 の 2つの設計を、 NUnit で書き直した例を載せておきます。
→ 全ソース : ExternalDesignAndTDD_20090608.zip ( 9,263 バイト) ( C# 2008 Express + NUnit 2.5 用 )


◆ メソッド string BuildMessage(string targetName)

入力 出力
string targetName return
null (NullReferenceException)
"" (空文字) "Hello,  !"
"{foo}" (1文字以上) "Hello, {foo} !"

[Test(Description = "BuildMessage() のテスト1: 入力文字列が null のとき")]
public void BuildMessageTest01_Null()
{
  try
  {
    string targetName = null;
    string message = ExternalDesignOfMethod01.BuildMessage(targetName);
    Assert.Fail("NullReferenceException を期待したが、出なかった。");
  }
  catch (NullReferenceException)
  {
    // TEST OK !
  }
}

[Test(Description = "BuildMessage() のテスト2: 入力文字列が空文字のとき")]
public void BuildMessageTest02_Empty()
{
  string targetName = string.Empty;
  string message = ExternalDesignOfMethod01.BuildMessage(targetName);
  Assert.AreEqual("Hello,  !", message);
}

[Test(Description = "BuildMessage() のテスト3: 入力文字列が上記のいずれでもないとき")]
public void BuildMessageTest03_NotEmpty()
{
  string targetName = "試験";
  string message = ExternalDesignOfMethod01.BuildMessage(targetName);
  Assert.AreEqual("Hello, 試験 !", message);
}


◆ メソッド string GetGreet(int hour)

入力 出力
int hour return
負の値 (ArgumentOutOfRangeException)
0 <= hour < 5 "Good night"
5 <= hour < 10 "Good morning"
10 <= hour < 18 "Hello"
18 <= hour < 20 "Good evening"
20 <= hour < 24 "Good night"
24 <= hour (ArgumentOutOfRangeException)

[Test(Description = " GetGreet() のテスト1: 入力時刻が負の値のとき")]
[ExpectedException("System.ArgumentOutOfRangeException")]
// 例外を期待するテストを ExpectedException 属性で書いてみる
public void GetGreetTest01_Minus()
{
  int hour = -1;
  string greet = ExternalDesignOfMethod01.GetGreet(hour);
}

[Test(Description = " GetGreet() のテスト2: 入力時刻が 0 以上 5 未満のとき")]
public void GetGreetTest02_0To5()
{
  // 境界値1: 下限
  Assert.AreEqual("Good night", ExternalDesignOfMethod01.GetGreet(0));

  // 代表値
  Assert.AreEqual("Good night", ExternalDesignOfMethod01.GetGreet(3));

  // 境界値2: 上限
  Assert.AreEqual("Good night", ExternalDesignOfMethod01.GetGreet(4));
}

[Test(Description = " GetGreet() のテスト3: 入力時刻が 5 以上 10 未満のとき")]
[TestCase(5)]
[TestCase(7)]
[TestCase(9)]
// 範囲の上下限・代表値をテストするのに、 TestCase 属性を使ってみる。
// テストの内容としては、上の「テスト2」と同等。
public void GetGreetTest03_5To10(int hour)
{
  Assert.AreEqual("Good morning", ExternalDesignOfMethod01.GetGreet(hour));
}

[Test(Description = " GetGreet() のテスト4: 入力時刻が 10 以上 18 未満のとき")]
[TestCase(10)]
[TestCase(12)]
[TestCase(17)]
public void GetGreetTest04_10To18(int hour)
{
  Assert.AreEqual("Hello", ExternalDesignOfMethod01.GetGreet(hour));
}

[Test(Description = " GetGreet() のテスト5: 入力時刻が 18 以上 20 未満のとき")]
[TestCase(18)]
[TestCase(19)]
public void GetGreetTest05_18To20(int hour)
{
  Assert.AreEqual("Good evening", ExternalDesignOfMethod01.GetGreet(hour));
}

[Test(Description = " GetGreet() のテスト6: 入力時刻が 20 以上 24 未満のとき")]
[TestCase(20)]
[TestCase(21)]
[TestCase(23)]
public void GetGreetTest06_20To24(int hour)
{
  Assert.AreEqual("Good night", ExternalDesignOfMethod01.GetGreet(hour));
}

[Test(Description = " GetGreet() のテスト7: 入力時刻が 24 以上のとき")]
[ExpectedException("System.ArgumentOutOfRangeException")]
public void GetGreetTest07_Over23()
{
  string greet = ExternalDesignOfMethod01.GetGreet(24);
}

なお、 ここではテストメソッドの名前を工夫して、 NUnit のテストランナー上で入力パターンに漏れが無いか分かりやすいようにしています。 ( 下図 )
Test 属性の引数 Description に指定した文字列は、 NUnit の GUI では一覧で見えないようで、 テストメソッドごとにプロパティを表示させて見なければなりません。 ( Visual Studio では、 一覧できます。 )
Program_design05_01

|

« [XP SP3] リモートデスクトップのクライアントがバージョンアップしてたんですねぇ | トップページ | USB 3.0 ではコネクタ形状が変わる »

プログラミング」カテゴリの記事

コメント

この記事へのコメントは終了しました。

トラックバック


この記事へのトラックバック一覧です: [プログラム設計事始] メソッドの外部設計とテストファースト(1):

« [XP SP3] リモートデスクトップのクライアントがバージョンアップしてたんですねぇ | トップページ | USB 3.0 ではコネクタ形状が変わる »