.NET Tools
NUnit入門 Test Firstのススメ [NUnit 2.0対応版]

2.何はさておき、最初にテストを書こう

(株)ピーデー 川俣 晶
2003/04/26

何はさておき、最初にテストを書こう

 NUnitにおけるテストとは、テスト対象となるクラス・ライブラリに含まれるメソッドなどを呼び出し、結果をチェックするメソッドの集合体である。テストを実行するためのユーザー・インターフェイスはNUnit側で用意しているので、プログラマーが特に作り込む必要はなく、ただ呼び出してチェックするコードだけを書けばよい。

 クラスに含まれるメソッドの中で、テスト・メソッドとなるのは、NUnit.Framework.Test属性を付けたものだけなので、名前は自由に付けてよい。とはいえ、テスト対象の機能を連想しやすい名前を付けておくに越したことはないだろう。なお、NUnitの過去のバージョンとの互換性のため、メソッド名の最初の4文字が“Test”のメソッドも、テスト・メソッドとして実行される。ともかく、NUnit.Framework.Test属性つまり、先頭に[Test]を付けたメソッドをどんどん書いていけば、テストを作成することができるわけである。

 というわけで、以下のようにテスト用のメソッドを追加してみた。

using System;
using NUnit.Framework;
using ServerApp;

namespace ServerAppTest
{
  [TestFixture]
  public class MaxDataSizeRecorderTest
  {
    [Test]
    public void Constructor()
    {
      MaxDataSizeRecorder recorder = new MaxDataSizeRecorder();
      Assertion.AssertEquals( "recorder.Name", "NoName", recorder.Name );
      Assertion.AssertEquals( "recorder.Point", 0, recorder.Point );
    }

    [Test]
    public void SetPoint()
    {
      MaxDataSizeRecorder recorder = new MaxDataSizeRecorder();
      recorder.SetPoint( "Taro", 1234 );
      Assertion.AssertEquals( "recorder.Name", "Taro", recorder.Name );
      Assertion.AssertEquals( "recorder.Point", 1234, recorder.Point );

      recorder.SetPoint( "Jiro", 1233 );
      Assertion.AssertEquals( "recorder.Name", "Taro", recorder.Name );
      Assertion.AssertEquals( "recorder.Point", 1234, recorder.Point );

      recorder.SetPoint( "Saburo", 1235 );
      Assertion.AssertEquals( "recorder.Name", "Saburo", recorder.Name );
      Assertion.AssertEquals( "recorder.Point", 1235, recorder.Point );
    }

    [Test]
    public void IsHighest()
    {
      MaxDataSizeRecorder recorder = new MaxDataSizeRecorder();
      recorder.SetPoint( "Taro", 1234 );
      Assertion.Assert( "recorder.IsHighest(1235)", recorder.IsHighest(1235) );
      Assertion.Assert( "!recorder.IsHighest(1234)", !recorder.IsHighest(1234) );
      Assertion.Assert( "!recorder.IsHighest(1233)", !recorder.IsHighest(1233) );
    }
  }
}
テスト用のメソッドを追加したMaxDataSizeRecorderTestクラス(C#版)
テスト用のメソッドにはNUnit.Framework.Test属性を設定する。この属性が付けられたメソッドはNUnitによって実行される。
 
Imports NUnit.Framework
Imports ServerApp

<TestFixture()> _
Public Class MaxDataSizeRecorderTest
  <Test()> _
  Public Sub Constructor()
    Dim recorder As MaxDataSizeRecorder = New MaxDataSizeRecorder()
    Assertion.AssertEquals("recorder.Name", "NoName", recorder.Name)
    Assertion.AssertEquals("recorder.Point", 0, recorder.Point)
  End Sub

  <Test()> _
  Public Sub SetPoint()
    Dim recorder As MaxDataSizeRecorder = New MaxDataSizeRecorder()
    recorder.SetPoint("Taro", 1234)
    Assertion.AssertEquals("recorder.Name", "Taro", recorder.Name)
    Assertion.AssertEquals("recorder.Point", 1234, recorder.Point)

    recorder.SetPoint("Jiro", 1233)
    Assertion.AssertEquals("recorder.Name", "Taro", recorder.Name)
    Assertion.AssertEquals("recorder.Point", 1234, recorder.Point)

    recorder.SetPoint("Saburo", 1235)
    Assertion.AssertEquals("recorder.Name", "Saburo", recorder.Name)
    Assertion.AssertEquals("recorder.Point", 1235, recorder.Point)
  End Sub

  <Test()> _
  public sub IsHighest()
    Dim recorder As MaxDataSizeRecorder = New MaxDataSizeRecorder()
    recorder.SetPoint("Taro", 1234)
    Assertion.Assert("recorder.IsHighest(1235)", recorder.IsHighest(1235))
    Assertion.Assert("!recorder.IsHighest(1234)", Not recorder.IsHighest(1234))
    Assertion.Assert("!recorder.IsHighest(1233)", Not recorder.IsHighest(1233))
  End Sub
End Class
テスト用のメソッドを追加したMaxDataSizeRecorderTestクラス(VB.NET版)

 Constructorメソッドは、コンストラクタをテストするものである。このメソッドではまず、テストの対象となるMaxDataSizeRecorderクラスのインスタンスを生成する。

 続いて記述されているAssertion.AssertEqualsとは、NUnit.Framework名前空間のAssertionクラスのAssertEqualsメソッドである。これは、C言語のassertや、.NET Frameworkクラス・ライブラリのSystem.Diagnostics.Trace.Assertメソッドと同じように、実行中に値の妥当性をチェックする機能を持つ。しかし、これらのメソッドが一般的にプログラムの通常実行時に使われるのに対して、ここで使われているメソッドは、NUnit 2.0でテスト実行中に判定結果を伝える機能を持っている。つまり、NUnit 2.0専用のテスト用のメソッドということである。通常、NUnit 2.0を用いてテストを行う場合は、System.Diagnostics.Trace.Assertメソッドではなく、NUnit 2.0で用意されたこれらのメソッドを使用する。NUnit 2.0のAssertionクラスにはこのためのさまざまなメソッドが用意されていて、AssertEqualsメソッドはその中の1つである。

 このAssertEqualsメソッドは、2つの値を比較して、それが同じであることを調べる役割を持つ。つまり、期待される値と、実際に処理結果となった値を渡して、値が一致すれば成功、値が一致しなかったときに失敗と記録する機能を持つ。ここで使用しているのは、引数を3つ持つバリエーションで、最初の引数がメッセージ、2番目の引数が期待される値、3番目の引数に実際の値を渡す。最初の引数のメッセージには、それが何をテストしているかを示す分かりやすい説明を書くとよい。こうしておくと、テストの結果を見るときに分かりやすくなる。

 ここでは、AssertEqualsメソッドを使って、メンバNameの初期値は"NoName"であるべきこと、そしてメンバPointの初期値は0であるべきことが示されている。

 もう1つ注目すべきことは、この時点で、テスト対象がMaxDataSizeRecorderという名前のクラスであること、そして、文字型のNameと数値型(整数型)のPointをメンバに持つことが確定されていることだ。つまり、テストを書くためにはあいまいさを取り除かなければならず、矛盾があってはならない。Test Firstを実践している場合、設計段階であいまいさや矛盾が入り込んでいても、この段階でそれを発見できる。コーディングが進んでから気付いて、せっかく書いたコードが無駄になる危険を減らすことができるのである。また、コーディングしていて正しい仕様が分からなくなったときも、テストのコードを見て疑問を晴らすことができる。人間の言葉はとかくあいまいになりがちだが、テスト・メソッドはプログラム言語で書くので、あいまいさが入り込みにくいのである。

 同じように2つのテスト・メソッドが続いている。[Test]SetPointメソッドは、主にMaxDataSizeRecorder.SetPointメソッドの結果を試すものだ。ここでは、条件を変えてMaxDataSizeRecorder.SetPointメソッドを3回呼び出している。このうち1回目は、最初に設定した情報はそのまま登録されるべきことを示している。2回目はわざと、1回目より低いポイントを渡しているが、これは「より低いポイントは登録されてはならない」という仕様を確認するためのものだ。そして、逆により高いポイントなら登録されるべきことを、3番目のSetPointメソッドの呼び出しが確認している。

 最後の[Test]IsHighestメソッドは、あるポイントが最大ポイントを超えているか否かを調べてbool値を返すMaxDataSizeRecorder.IsHighestメソッドを確認している。ここでは、仮に設定したポイントに対して、大きいとき、同じとき、小さいときのそれぞれのケースで、正しいbool値を返しているかを確認している。なお、このメソッドでは、AssertEqualsメソッドではなく、Assertメソッドを使用しているが、これはAssertEqualsメソッドと同様に役割を持っている。しかし、ある式の値がtrueであることを調べる、という機能面での相違があるものである。

 以上のテスト・メソッドをパスすれば、最大データ・サイズ記録クラスは正常に機能していると考えてよいだろう。もちろん、もっと厳密に試そうとすれば、やるべきことは山ほどある。例えば、ここでは渡すポイント値として代表的な数値を試しているにすぎないが、これは完全な正しさの保証にならない。テストに使わなかった値で誤動作する可能性を排除できないからだ。そこで、あらゆる可能性のある値を渡してチェックするようにテスト・メソッドを作成する方が望ましいように思えるかも知れない。しかし、Test Firstでは、それを望ましいとは考えない。というのは、テスト・メソッドはソースを修正するごとに何度も繰り返し実行するものと考えられているからである。そのため、負担にならない時間で実行できる高速性は必須要件であり、それは100%の完璧さよりも優先される。プログラマーが重要だと考える条件をテストしていれば、完璧でなくても多くの問題を検出することができる。そして、気軽に何度も繰り返し実行されるテストは問題を早期に発見し、こじれる前に対処するチャンスを与えてくれるのである。

 逆に完璧を目指しても、ろくなことはない。時間と手間が採算に合わないほど増える場合も多いし、テスト・メソッドが複雑化してそれ自身がバグを含む危険も増大する。さらに、現実的な時間内ですべてのケースをテストできない場合もあり得る。もし、完璧なテストが実現できても、処理に時間が掛かるとすれば滅多に実行されず、テストにパスしないコードが長期間温存されてしまうリスクを背負うことになる。

コラム:NUnitで例外をチェックする
 あるメソッドに期待される処理結果が、何かの数値や文字列などではなく、例外である場合もある。つまり、ある条件でメソッドを実行したとき、正しい処理結果は特定の例外を投げることである、という場合もある。そういう場合は、ExpectedException属性を使って、それを示すことができる。以下は、System.DivideByZeroException例外が起きることが期待されたケースをテストするサンプル・コードである。

using System;
using NUnit.Framework;

namespace ExceptionTest001
{
  [TestFixture]
  public class ExceptionTest001
  {
    private int div( int a, int b )
    {
      return a/b;
    }
    [Test]
    [ExpectedException(typeof(DivideByZeroException))]
    public void ExceptionSample()
    {
      div( 1, 0 );
    }
  }
}
DivideByZeroException例外の発生をテストするメソッド例(C#版)
正常時に例外を発生するような処理をテストするには、ExpectedException属性を使用する。
 
Imports NUnit.Framework

<TestFixture()> _
Public Class Class1
  Private Function div(ByVal a As Integer, ByVal b As Integer) As Integer
    Return a \ b
  End Function
  <Test(), _
  ExpectedException(GetType(DivideByZeroException))> _
     Public Sub ExceptionSample()
    div(1, 0)
  End Sub
End Class
DivideByZeroException例外の発生をテストするメソッドの記述例(VB.NET版)

 ExpectedException属性の引数には、期待される属性の型をSystem.Type型の値で指定する。メソッドの実行中にその型の例外が起これば成功、起こらなければ失敗である。

 

 INDEX
  [.NET Tools]
  NUnit入門 Test Firstのススメ [NUnit 2.0対応版]
     1.NUnitの環境を準備する
   2.何はさておき、最初にテストを書こう
     3.空のメソッドを書いてNUnitを実行してみる
     4.プログラム本体を実装する
     5.唐突な仕様変更に対応する
 
インデックス・ページヘ  「.NET Tools」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間