M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

NUnitクイックスタート

今さらと言えばまったく今さらなのですが、NUnitについてちょっと調べていました。それで例によって稚拙な翻訳ですが載せておきます。原文は、“NUnit Quick Start”(2011年3月23日取得)です。

******************************

NUnitクイックスタート

註:このページはNUnitの初期のリリース時に作成されたQuickStart.docをもとにしています。ここで紹介されているコードサンプルは、テスト駆動開発(TDD)の例としては決してよいものではありません。にもかかわらずこうしてここに掲載を続けているのは、このドキュメントがNUnitの基本的な使用方法を解説しているからです。私たちは将来のリリースで、このドキュメントを改訂もしくは置き換えるつもりです。

簡単な例からはじめましょう。私たちはある銀行システムを開発しており、基本的なドメインクラスとしてAccountクラスを製造したと仮定してみましょう*1。Accountクラスは、預け入れ、引き出し、そして送金に対応しています。Accountクラスは、だいたい次のような感じです。

namespace bank
{
  public class Account
  {
    private float balance;
    public void Deposit(float amount)
    {
      balance+=amount;
    }

    public void Withdraw(float amount)
    {
      balance-=amount;
    }

    public void TransferFunds(Account destination, float amount)
    {
    }

    public float Balance
    {
      get{ return balance;}
    }
  }
}

では、このクラスのためのテストケースAccountTestを作成しましょう。最初にテストするのはTransferFundsです。

namespace bank
{
  using NUnit.Framework;

  [TestFixture]
  public class AccountTest
  {
    [Test]
    public void TransferFunds()
    {
      Account source = new Account();
      source.Deposit(200.00F);
      Account destination = new Account();
      destination.Deposit(150.00F);

      source.TransferFunds(destination, 100.00F);
      Assert.AreEqual(250.00F, destination.Balance);
      Assert.AreEqual(100.00F, source.Balance);
	
    }
  }
}

まず注目していただきたいのは、AccountTestクラスには[TestFixture]属性が指定されていることです。これはこのクラスがテストコードを含んだものであることを示すためのものです(この属性は継承可能です)。クラスはpublicでなければなりません。親クラスに関する制約がないことも条件です*2。また、このクラスはデフォルトコンストラクタを持っている必要があります *3

AccountTestクラスの唯一のメソッドであるTransferfundsには、[Test]属性が指定されています。この属性は、そのメソッドがテストメソッドであることを示すものです。テストメソッドは、引数を持ちません*4。引数はとりません。今回作成したテストメソッドでは、まずテスト対象のオブジェクトの初期化処理を行い、処理メソッドを実行し、最後にオブジェクトの状態をチェックしています。Assertクラスは、事後条件をチェックするためのメソッドのコレクションであり、今回の例では2つのAccountの間での送金のあとの状態を確認するためにAreEqualメソッドが仕様されています(このメソッドにはいくつかのオーバーロードメソッドが存在しますが、今回の例では、第1引数には期待される値を、第2引数には実際の値を指定しています)。

AccountTestをコンパイルして、実行してみましょう。ここではコンパイルした結果できたファイルがbank.dllという名前であったと仮定します。NUnit Gui(インストーラが、ショートカットをデスクトップと“Program Files”フォルダに作成しているはずです)を起動します。メニューから[File]→[Open]と進み、[Open]ダイアログボックスでblank.dllを選択します。bank.dllが読み込まれると、NUnit Guiの画面左側にテストのツリー構造が、また右側には状態パネルが表示されます。[Run]ボタンをクリックすると、ステータスバー*5と画面左側のTransferFundsノードが赤色に変化します。テスト結果は、失敗となりました。「エラーと失敗」(Errors and Failures)パネルには次のメッセージが表示されています。

TransferFunds : expected <250> but was <150>
and the stack trace panel right below it reported where in the test code the failure has occurred –
at bank.AccountTest.TransferFunds() in C:\nunit\BankSampleTests\AccountTest.cs:line 17

予想通りの結果です。何しろ私たちはまだTransferFundsメソッドを実装していません。それでは今度はテストが成功するようにしましょう。NUnitの画面は閉じないでください。開発環境の画面に戻り、コードを修正します。TransferFundsメソッドは次のような実装になりました。

public void TransferFunds(Account destination, float amount)
{
	destination.Deposit(amount);
	Withdraw(amount);
}

この状態でAccountTestクラスを含むコードを再度コンパイルし、NUnitの画面で[Run]をクリックします。ステータスバーとテスト・ツリーは緑色になりました。(NUnit GuiはAccounttestクラスを含むアセンブリファイルを自動的に再読込したことに注意してください。NUnit Guiの画面を開いたまま、IDEのコーディングを続け、引き続きテストを実施していくことができます。)

それではAccountクラスのコードにいくつかのエラーチェック機能を追加しましょう。私たちはAccountクラスに、取り引き手数料を確保するための最低預金残高条件を追加しようとしています。最低預金残高のプロパティを追加しましょう。

private float minimumBalance = 10.00F;
public float MinimumBalance
{
	get{ return minimumBalance;}
}

引き出し超過を知らせるためは例外クラスを使用しましょう。

namespace bank
{
  using System;
  public class InsufficientFundsException : ApplicationException
  {
  }
}

AccountTestクラスにも新しいテストメソッドを追加します。

[Test]
[ExpectedException(typeof(InsufficientFundsException))]
public void TransferWithInsufficientFunds()
{
	Account source = new Account();
	source.Deposit(200.00F);
	Account destination = new Account();
	destination.Deposit(150.00F);
	source.TransferFunds(destination, 300.00F);
}

このテストメソッドには[Test]属性に加えて、[ExpectedException]属性が指定されています。この属性は、テストコードが何らかの例外を発生させるはずのものであることを示しています。実行時に例外が発生しなかった場合、テストは失敗したものと見なされます。ソースコードをコンパイルし、NUnitの画面に戻りましょう。テストコードをコンパイルし直したため、画面上のテスト・ツリーはグレーアウトして表示されています。まだテストを実施していなかった場合、テスト・ツリーは閉じられた状態で表示さてています(NUnit Guiはテストコードを含むアセンブリファイルの変更を監視し、ファイルが更新された際にはテスト・ツリーに反映されます。例えば新しくテストメソッドが追加された場合などです)。[Run]ボタンをクリックすると、ステータスバーはまた赤色になりました。次のようなメッセージが表示されているはずです。

AccountのTransferFundsメソッドのコードを再度修正しましょう。

public void TransferFunds(Account destination, float amount)
{
	destination.Deposit(amount);
	if(balance-amount<minimumBalance)
		throw new InsufficientFundsException();
	Withdraw(amount);
}

コンパイルし、テストを実行します。今度は緑色のバーが表示され、てすとが成功しました。でもちょっとまってください。今しがた私たちが書いたコードを見ると、この銀行は送金手続きが失敗する度にお金をなくしているように見えます。テストコードを書いて、確かめてみましょう。

[Test]
public void TransferWithInsufficientFundsAtomicity()
{
	Account source = new Account();
	source.Deposit(200.00F);
	Account destination = new Account();
	destination.Deposit(150.00F);
	try
	{
		source.TransferFunds(destination, 300.00F);
	}
	catch(InsufficientFundsException expected)
	{
	}

	Assert.AreEqual(200.00F,source.Balance);
	Assert.AreEqual(150.00F,destination.Balance);
}

このコードは、処理メソッドによって取り引きされる資産を検証しています。すべての作業が成功するか、あるいは失敗するかです。コンパイルして実施すると…ステータスバーは赤くなりました。私たちは架空の300ドルをつくってしまいました。送金もとのAccountの残高は200ドルでしたが、送金先のAccountには450ドルが入っているのです。さてどうしたものでしょう? 最低預金残高のチェックを、残高更新処理より前にもってくればいいのです。

public void TransferFunds(Account destination, float amount)
{
	if(balance-amount<minimumBalance) 
		throw new InsufficientFundsException();
	destination.Deposit(amount);
	Withdraw(amount);
}

あるいは、Withdraw()メソッドが別の例外を投げるのはどうでしょう? catchブロックのなかで求償取引(訳者:ここでは、ようするに辻褄のあった送金)を実行したり、あるいは取引管理者のオブジェクト状態管理の手腕をあてにすべきなのでしょうか? こうした問題にいずれ結論を出さなくてはならないわけですが、それはともかく、この失敗するテストケースをどうすべきでしょうか。削除しますか? よりよい方法は一時的にテストケースではなくしてしまうことです。次の属性をこのテストメソッドに指定します。

[Test]
[Ignore("トランザクション管理の実装方法を決めること")]
public void TransferWithInsufficientFundsAtomicity()
{
	// テストコードは同じもの
}

テストクラスをコンパイルし実行すると、ステータスバーは黄色くなりました。“Test Not Run”(実行されないテスト)タブをクリックすると、テスト対象から除外されたbank.AccountTest.TransferWithInsufficientFundsAtomicity()が一覧にのっていることが分かります。

いくつかの修正を経てテストコードは整ってきました。すべてのテストメソッドは、テスト対象オブジェクトを共有しています。セットアップメソッドの中に初期化処理を記述し、これをすべてのテストメソッドから利用するようにしましょう。改善されたテストクラスは次のようになりました。

namespace bank
{
  using System;
  using NUnit.Framework;

  [TestFixture]
  public class AccountTest
  {
    Account source;
    Account destination;

    [SetUp]
    public void Init()
    {
      source = new Account();
      source.Deposit(200.00F);
      destination = new Account();
      destination.Deposit(150.00F);
    }

    [Test]
    public void TransferFunds()
    {
      source.TransferFunds(destination, 100.00f);
      Assert.AreEqual(250.00F, destination.Balance);
      Assert.AreEqual(100.00F, source.Balance);
    }

    [Test]
    [ExpectedException(typeof(InsufficientFundsException))]
    public void TransferWithInsufficientFunds()
    {
      source.TransferFunds(destination, 300.00F);
    }

    [Test]
    [Ignore("Decide how to implement transaction management")]
    public void TransferWithInsufficientFundsAtomicity()
    {
      try
      {
        source.TransferFunds(destination, 300.00F);
      }
      catch(InsufficientFundsException expected)
      {
      }

      Assert.AreEqual(200.00F,source.Balance);
      Assert.AreEqual(150.00F,destination.Balance);
    }
  }
}

Initメソッドが、共通の初期化コードを保持していることに注意してください。メソッドには返値も、引数もありませんが、[SetUp]属性が指定されています。コンパイルして実行すると、同じようにステータスバーは黄色くなりました。

******************************

*1:訳者:ドメインクラスとは、おそらく、現実世界や現実世界のある分野のなかでし使用される概念・ヒト・モノを、プログラミング言語のクラスとして表現したものです。この場合でいえば、Accountクラスはもちろん現実世界における“口座”に対応しているわけです。

*2:訳者:C#や.NETについての経験に乏しいので、ここで言及されている「親クラスに関する制約」(restrictions on its superclass)が、何を指しているのか判断がつきません。

*3:訳者:デフォルトコンストラクタは、コンパイラにより自動で追加される引数を持たないコンストラクタです。

*4:訳者:原文では、「voidを返す」という表現になっています。

*5:訳者:ウィンドウ下部にあるものではなく、NUnit Guiの画面右側上部に表示されるものです。