C# 2008 Express + NUnit 2.5 で、 初めてのテストファースト Step by Step

※ 初出: biac の それさえもおそらくは幸せな日々@nifty 「NUnit の "Hello, world!" ~ C# 2008 Express + NUnit 2.5 で、 テストファーストの Step by Step」 ( 2009/05/27 )

 

NUnit をインストールして動作確認までできた ので、 次は、 NUnit を使ってテストファーストでプログラムを作る方法についてです。

開発環境としては、 Visual Studio 2008 Express Edition SP1 の C# を使います。
なお、 Visual Studio 2008 の Pro. 版以上には、 MS 独自の単体テスト機能がありますが、 基本的な考え方は NUnit を使う場合と変わりません。

テストファーストでプログラムを作っていく方法 ( TDD : Test Driven Development ) は、 次のような流れになります。

[0] 作成したいメソッド ( および、 そのメソッドを含むクラス ) の仕様を考える。
  ↓
作成したいメソッドの外部設計書を記述する。 ( 省略可。 というより、 TDD に慣れてしまえば不要。 )
  ↓
[1] 作成したいメソッドをテストするコードを Visual Studio などで作成する。
  ↓
NUnit ( または、 Visual Studio の単体テスト機能 ) で、 テストを実行する。 (作成したいメソッドはまだ完成していないので) もちろん、 失敗する。 【レッド】
  ↓
[2] 作成したいメソッドを Visual Studio などで書き、 ビルドする。
  ↓
[3] NUnit ( または、 Visual Studio の単体テスト機能 ) で、 テストを実行する。
テストが失敗したら、 [1] または [2] に戻る。
テストが成功したら次へ進む。 【グリーン】
  ↓
[4] 作成したメソッドの内部をきれいに書き直す。 またテストを実行し、 メソッドの振る舞いが変わっていないことを確認する。 【リファクタリング】
  ↓
[0] に戻る。

特徴:
・ コードを書き始める前に、 メソッド単位までプログラムを分解する。 ( プログラムの内部設計 )
・ そのメソッドの振る舞いが完全に把握できるレベルまで、 細かく分解する。 ( メソッドの外部設計 )
   
※ 把握できていれば、 メソッドの外部設計書を書けるはず。
・ メソッドの外部設計を、 テストコードとして表現する。 ( テストの自動化 )
・ 先にテストコードを作るので、 そのテストは (一応) ブラックボックステストになる。
 
※ テストコードを書く時点で、 たいていはメソッドの内部構造も 「見えて」 いますから、 「一応」 です。
・ テストはすぐに何度でも自動実行できるので、 リファクタリングを実施してコードをきれいに保つことが出来る。

TDD では、 レッド → グリーン → リファクタリング → … のリズムを大切にします。 「波に乗った」 状態になると、 集中力を持続したまま、 効率良く開発が進むからです。
また、 TDD では、 テストコードを作る前の手順は規定していません。 慣れた人は、 いきなりテストコードを書きます。 不慣れなうちは、 プログラムの内部設計 ( 文章、 クラス図、 シーケンス図 etc. ) のドキュメントを作ったり、 メソッドの外部設計書を書いたりして、 これから作るメソッドの外部設計をきちんと把握してから、 それをテストコードに変換するようにするとよいでしょう。

それでは、 最初に作るプログラムのお約束となっている "Hello, world!" に取り組んでみましょう。

メソッドの外部設計までは、 出来上がっているものとします。
"Hello, world!" という文字列を返してくれる GetMessage() メソッドです。

ソリューション: HelloNUnit
プロジェクト: HelloNUnit ( Windows Form )
クラス: public Greeting
メソッド: public static string GetMessage()

入力 出力
(無し) 返値 string
"Hello, world!"

 

◆ [1] 作成したいメソッドをテストするコードを Visual Studio などで作成する。

Visual C# 2008 を起動します。
20090526nunit01

※ Express Edition は、 初期状態ではビルド構成 ( Debug / Release など ) を選択できません。 メニュー [ツール] - [オプション] で、 設定を確かめておいてください。
20090526nunit12a
左下の 「すべての設定を表示」 にチェックを入れてから、 左の [全般] を選び、 右の 「ビルド構成の詳細を表示」 にチェックを入れます。

メニュー [ファイル] - [新しいプロジェクト...] で、 [Windows フォーム アプリケーション] を選び、 「プロジェクト名」 には "HelloNUnit" と指定して、 [OK] します。
20090526nunit02
20090526nunit03
ソリューション名も同じ "HelloNUnit" として、 ソリューションとプロジェクトが生成されます。

ここでいったん、 Ctrl-S を押して、 適当な場所に保存しておきます。
20090526nunit04
「プロジェクトの保存」 ダイアログでは、 「ソリューションのディレクトリを作成」 にチェックを入れたままにして、 保存します。

次に、 テストコード用のプロジェクトを追加します。
ソリューションエクスプローラー で [ソリューション 'HelloNUnit'] を右クリックし、 出てきたコンテキストメニューから [追加] - [新しいプロジェクト…] を選びます。
20090526nunit05a
「新しいプロジェクトの追加」 ダイアログで、 [クラス ライブラリ] を選び、 「プロジェクト名」 には "HelloNUnitTest" と指定して、 [OK] します。
20090526nunit06

Class1.cs が自動生成されています。
20090526nunit07
このファイル名とクラス名を GreetingTest に変更します。
※ これから作成したいメソッド ( GetMessage() ) を含むクラス ( Greeting ) の名前 + "Test" というクラス名にします。
ソリューションエクスプローラー で Class1.cs を右クリックし、 [名前の変更] を選びます。
20090526nunit08a
"Class1.cs" を "GreetingTest.cs" に書き換えて Enter を押すと、 「 (前略…) このプロジェクトのコード要素 'Class1' への参照をすべて変更しますか?」 と尋ねられるので、 [はい] と答えます。
20090526nunit09a
すると、 コード中のクラス名も自動的に修正されます。
20090526nunit10a

次に、 HelloNUnitTest プロジェクトで、 NUnit.framework を使うための参照設定をします。
今回は、 nunit.framework だけを追加すればよいです。
20090526nunit11

 

では、 テストメソッドを作ります。
using 句に NUnit.Framework を追加します。
空のテストメソッドを追加します。 これから作成したいメソッドは GetMessage() ですから、 テストメソッドは GetMessageTest() とし、 次のように記述します。

[Test(Description = "常に 'Hello, world!' が返る")]
public void GetMessageTest()
{
  Assert.Inconclusive("テスト未実装");
}

※ Assert.Inconclusive() は、 テストプロジェクトがちゃんと出来ているか、 確認するために入れました。 通常は、 テストメソッドの中身をいきなり書いていきます。
テストメソッドを自動生成するテンプレートなどを作るときには、 テストの実装漏れを失くすために Assert.Inconclusive() を入れるようにしておくとよいでしょう。
※※ 中身がからっぽのテストメソッドは、 NUnit でテストすると ( 実際には何もテストしていないのに) パスしてしまいます。 ( Visual Studio の単体テスト機能も同じ )

では、 確認のため、 一度ビルドしてみましょう。 Debug ビルドを行います。
20090526nunit13a
標準のツールバーを出していれば、 この画面のようにツールバーに、 現在のビルド構成が表示されています。 もし Release になっていたら、 Debug に切り替えてください。
標準のツールバーを出していないときは、 メニュー [ビルド] - [構成マネージャ…] で、
「アクティブソリューション構成」 を Debug にします。
※ 前述した 「ビルド構成の詳細を表示」 オプションが ON になっていないと、 Debug / Release の切り替えは選べません。

ビルドに成功したら、 NUnit 2.5 を起動し、 [File] - [Open Project...] で HelloNUnitTest.csproj を読み込ませます。
※ *.csproj ファイルが表示されないときは、 NUnit のオプション設定をします。 ⇒ 「NUnit 2.5 がリリースされているので、 Windows 7 RC に入れてみた。

そのまま、 [Run] ボタンをクリックして、 テストを実行してみます。
20090526nunit14
GetMessageTest に、 黄色く 「(?)」 マークが付きます。
Assert.Inconclusive() が記述されていると、 そのテストは inconclusive ( 決定的でない; うやむや ) である、 つまりテスト結果は不明だということになるのです。

なお、 左側で GetMessageTest を右クリックして [Properties] を選ぶと、 Test 属性に指定した Description パラメータや、 Assert.Inconclusive() に渡したメッセージを見ることができます。

以上で、 テストを書いてちゃんと実行できる状態になっていることが、 確認できました。

それでは、 テストを書いてみます。

メソッドの外部設計は、 「Greeting クラスのスタティックメソッド GetMessage() を呼び出すと、 "Hello, world!" という文字列が返ってくる」 ということでしたね。
これを、 そのままテストコードで表現するわけです。

そのためには、 想定される返値と、 メソッドの実際の返値とを比較すればよいでしょう。
nunit.framework の Assert.AreEqual(object expected, object actual) というメソッドを使います。
最初の引数 ( expected ) は、 想定される結果です。
ふたつめの引数 ( actual ) に、 実際の結果を渡します。
すると、 こう書けますね。
Assert.AreEqual("Hello, world!", Greeting.GetMessage());

しかし、 「名前 'Greeting' は現在のコンテキスト内に存在しません。」 と怒られます。
20090526nunit15a
なぜでしょう? 理由は、 次の 3つです。
・ Greeting クラスを含んでいるはずの HelloNUnit プロジェクトに対する、 参照設定をやっていない。
・ Greeting クラスも、 まだ作っていない。
・ Greeting クラスを使うための using 句を書いていない。

※ この時点で、 上記のエラーが出ないようなら、 何かおかしいです。 作った覚えのない Greeting クラスが、 すでに存在している、 ということですからね。 なにか勘違いしているか、 すでに参照設定してあるライブラリのどこかに Greeting クラスが含まれているか…
先にテストから作り始めると、 そういう名前の衝突も先に検出できるわけです。

まず、 HelloNUnitTest プロジェクトに、 HelloNUnit プロジェクトへの参照を追加してください。
続いて、 HelloNUnit プロジェクトに Greeting クラスを追加します。
20090526nunit16a
20090526nunit17
そうしたら、 GreetingTest.cs の方に using 句を追加します。
using HelloNUnit;

すると、 エラーメッセージが変わったはずです。
20090526nunit18a
「エラー: 'HelloNUnit.Greeting' はアクセスできない保護レベルになっています。」
これは、 Greeting クラスは存在しているけれど、 ( そのスコープが internal なので ) HelloNUnitTest プロジェクトからは見えない、 ということですね。

ここまでで、 テストする側のコードは出来上がっています。
しかし、 まだビルドは通りませんから、 テストの状況としては失敗している ( = レッド ) 状態です。

 

◆ [2] 作成したいメソッドを Visual Studio などで書き、 ビルドする。

ビルドできなくてはテストを実行できませんので、 ここで目的の Greeting クラスに記述を入れます。
Greeting クラスの宣言行に "public" と追加すればいいですね。
20090526nunit19a
すると、 テストコードの Greeting のところのエラーは消え、 こんどは GetMessage() メソッドにエラーが移ります。
20090526nunit20a
「'HelloNUnit.Greeting' に 'GetMessage' の定義がありません。」

このエラーもあたりまえですね。 まだ GetMessage() メソッドを書いていないのですから。
※ これもまた、 エラーが出なかったとしたら、 何かを間違えています。 「こう書いたら、 IDE がこう怒ってくるはず」 と予測してからテストコードを書いて、 実際に予想通りの怒られ方をされることを確認しましょう。

では、 GetMessage() メソッドを書きます。
といっても、 コンパイルが通る最低限のことだけにします。 何か返さないとコンパイルエラーになってしまうので、 null を返すことにしましょうか。
public class Greeting
{
  public static string GetMessage()
  {
    return null;
  }
}

これで GreetingTest のほうもビルドが通るようになりました。
public class GreetingTest
{
  [Test(Description = "常に 'Hello, world!' が返る")]
  public void GetMessageTest()
  {
    Assert.AreEqual("Hello, world!", Greeting.GetMessage());
  }
}

ソリューション全体をビルドしておきます。

 

◆ [3] NUnit で、 テストを実行する。

ビルドに成功したら、 NUnit 2.5 を起動し、 [File] - [Open Project...] で HelloNUnitTest.csproj を読み込ませます。
※ [1] で NUnit を開いたままにしていた場合は、 ビルドしなおした時点で、 自動的に読み込みなおされているはずです。

[Run] ボタンをクリックしてテストを実行してみると…
20090526nunit21
…レッド、 テスト失敗です。

※ この失敗は予想していましたか? テストを実行する前に、 成功するか失敗するか予想するようにしましょう。
TDD のルールは、 テストを失敗させる → テストを通る最小限のコード修正を行う → テストに成功する というステップを踏みます。 こうすることで、 テストを通っていないコードや、 ムダなコードを書かないようにするわけです。

NUnit に表示された、 エラーメッセージとその場所は次のようになりました。

HelloNUnitTest.GreetingTest.GetMessageTest:
  Expected: "Hello, world!"
  But was:  null

場所 HelloNUnitTest.GreetingTest.GetMessageTest()
場所 D:\Users\biac\Documents\Visual Studio 2008\Projects\HelloNUnit\HelloNUnitTest\GreetingTest.cs:行 16

GreetingTest.cs の 16行というのは、 Assert.AreEqual() の行ですね。
そこで "Hello, world!" を期待していた ( Expected ) のだけれど、 しかしそうではなく ( But was ) null であった、 というわけです。

null が返ってきたのは、 GetMessage() の実装がそうなっているからですね。 [2] に戻って、 GetMessage() を修正しましょう。

 

 

◆ [2] 作成したいメソッドを Visual Studio などで書き、 ビルドする。

テスト結果から、 null を返しているところを、 "Hello, world!" を返すように修正すれば良いとわかります。

そのように修正して、 ビルドしましょう。
public class Greeting
{
  public static string GetMessage()
  {
    return "Hello, world!";
  }
}

 

◆ [3] NUnit で、 テストを実行する。

こんどは成功するはずです。
20090526nunit22
はい、 グリーンになりました。

 

◆ [4] 作成したメソッドの内部をきれいに書き直す。

プログラムとしてはちゃんと動くようになったので、 ソースコードをきれいにしておきます。
といっても、 今回はやることが無いと思いますけれど。

通常は、 最低限のこととして、 インデントや行間の入れ方などを整頓し、 変数名を見直します。
また、 プロジェクトのルールに従ったコメント作成なども、 このときに行います。
必要だと思ったときは、 リファクタリングも実施します。

 

 

◆ [3] NUnit で、 テストを実行する。

ソースコードをきれいに整頓できたら、 またテストを実行します。 テストは成功するはずですよね。

もしも失敗したら、 直前にやったコードの整理で何か勘違いをしたのです。 そのあたりを思い出しながら、 テストが通るようになるまで修正します。

※ ソースコードリポジトリ ( VSS や TFS など ) を使っている場合は、 リファクタリングの前に一度チェックインしておきましょう。

 

 

以上で、 テストファーストによる最初のメソッド作成は完了です。

テストを実施できるようになるまでの手順が多かったと思いますが、 この後、 テストを追加・修正していくときには、 もうそんなに手間は掛かりません。
試しに、 「 [プログラム設計事始] メソッドの外部設計(1)」 の string BuildMessage(string targetName) メソッドを作ってみてください。
※ HelloNUnitTest.cs にテストメソッド BuildMessageTest() を作ってから、 HelloNUnit.cs に BuildMessage() メソッドを書きます。

なお、 WinForm などの GUI 部分は、 いまのところ NUnit で簡単にテストする、 というわけにはいきません。 GUI 部分のコードをできるだけ少なくしておいて手動テストを行う、 というのが今は一般的でしょう。

HelloNUnit のフォームに、 ボタンがクリックされたら GetMessage() の結果をメッセージボックスで出すようなコードを追加してみました。 ⇒ 全ソースコード: HelloNUnit_20090527.zip   (15,187 バイト)
  20090526nunit23


Copyright © 2009 biac All rights reserved.