はじめに
開発時にいかに既存のコードにバグが混じらないようにするかは、これまでずっと大きな課題とされてきた。しかし今日では、テスト駆動型開発(Test Driven Development:TDD)という新しい手法により、この状況は変化しつつある。TDDの主な原則は次の2つである。
- 自動テストが失敗しない限り、1行もコードを書かない
- 重複を避ける
この原則はおおむね理にかなっている。多くの自動テストをボタンクリックやコマンドラインから実行したいと望んでいる開発者は多い。それによってアプリケーションの正常な動作を保証できるからだ。自動単体テスト実行用のフレームワークを実現するNUnitというオープンソースツールもある。このツールは、データ構造を操作するだけのクラスライブラリでは非常によく使われているが、GUIで利用するのはかなり難しい(NUnitの詳細については「Test Driven Development Using NUnit in C#」を参照)。
GUIのaspxページのテスト用フレームワークを実現するNUnitAspというオープンソースツールもある。NUnitAspはNUnitを拡張したもので、WebFormsで使用されるHttpContext
をテストするために必要な特別なクラスを提供する。これにより、NUnitテスト内で、QueryString、Session、ViewState、PostBack、Web.config
などを考慮しつつ、ユーザーとまったく同じ方法でページをロードできる。さらにプログラム的にイベントをトリガし、その結果のプロパティを調べることができる。これは、まったく新しい考え方のテスト技法である。
NUnitAspのWebサイトには、汎用的なチュートリアルが用意されているので、ここでは一から説明するのではなく、簡単なサンプルを紹介したのちに、NUnitAspを利用してより高度な問題を解決する方法について見ていく。コードサンプルは上記のリンクからダウンロード可能である。
簡単な例から
では、まずNUnitAspを使った「Hello, World!」的なテストを実行してみよう。正しいTDDでは、先にテストを記述し、そのテストに合格するようなコードを書くというのが基本だが、ここではNUnitAspの使い方をわかりやすく説明するために、先にアプリケーション部分を記述することにする。
まず、次の2つのプロジェクトを使った新しいソリューションを作成する。
- WebMain――ASP.NET Webアプリケーションプロジェクト
- UitGui――GUI用の単体テストと統合テストを含んだコンソールアプリケーション
このWebプロジェクトでは、Basic.aspx
という名前のWebFormを作成する。このフォームにはテキストボックス、ボタン、ラベルがそれぞれ1つずつ含まれている。ユーザーがボタンをクリックすると、テキストボックスの値がラベルに表示される。このページは、コードサンプルのSimple\Basic.aspx
に含まれている。
関連するメソッドは次の1つだけである。
private void Button1_Click(object sender, System.EventArgs e) { this.Label1.Text = this.TextBox1.Text; }
ここでプロジェクトを実行し、ページがきちんと動作するかを確認してみよう。このページをVisual Studioにロードできない場合は、おそらくNUnitAsp内でも実行できない。この簡単な例では、テキストボックス、ボタン、ラベルが正しく連動していることを確認しておく。
UitGuiプロジェクトでは、nunit.framework.dll
(バージョン2.2)とNUnitAsp.dll
(バージョン1.5.1)の両方に対する参照を追加する。これらのDLLはコードサンプルに含まれているほか、それぞれのWebサイトからもダウンロードできる。
さらにTestBasic.cs
という新しいクラスを追加する。このクラスをNUnitAspで実行するのに必要な手順は、次の通りである。
- NUnit DLLおよびSystem DLL内の名前空間に対するクラス参照を作成する。
- クラス宣言行に
[TestFixture]
属性を追加し、このクラスにテストが含まれていることをNUnitAspに指示する。さらに、このクラスをWebFormTestCase
クラスから派生させて、Webページのロード先となるBrowser
プロパティと、テスト条件を調べるためのアサーションを継承する。 - コンストラクタを
public
にし、NUnitAspなどの外部プログラムがこのクラスをインスタンス化してテストを呼び出せるようにする。
using System; using System.Web; using NUnit.Framework; using NUnit.Extensions.Asp; using NUnit.Extensions.Asp.AspTester;
[TestFixture] public class TestBasic : WebFormTestCase
public TestBasic() { }
今度は実際にテストを記述してみよう。まずはテストのコードを見てほしい。詳しい内容は後から説明する。
[Test] public void SelectDropdown() { string strURL = TestConstants.AppPath + "Simple/Basic.aspx"; Browser.GetPage(strURL); //declare the controls: ButtonTester Button1 = new ButtonTester("Button1", CurrentWebForm); LabelTester Label1 = new LabelTester("Label1", CurrentWebForm); TextBoxTester TextBox1 = new TextBoxTester("TextBox1", CurrentWebForm); //run through actions: string strValue = "Hello"; TextBox1.Text = strValue; Button1.Click(); //check values: WebAssertion.AssertEquals(strValue, Label1.Text); } //end of method
このテストコードは、NUnitの標準的なメソッド宣言から始まっている。このメソッドは[Test]
属性とvoid
の戻り値を必要とし、パラメータは受け取らない。次に、ロードするページのURLを生成し、それをBrowser
オブジェクトのGetPage()
メソッドに渡している。http://localhost/myprojects/
などのドメイン名をハードコーディングせず、定数クラスへと抽象化している点に注目してほしい。
Browser.GetPage()
メソッドにページを渡した後は、標準サーバーコントロールに対応するAspTester
オブジェクトを作成する。AspTester
のコンストラクタには、Asp IDとコンテナコントロール(CurrentWebForm
、ユーザーコントロールオブジェクトなど)を渡す必要がある。
重要なのは、AspTester
オブジェクトを作成した後の部分である。これらのAspTester
オブジェクトにより、実際のユーザーと同様の方法で、ページ内にあるコントロールの大部分のプロパティにプログラム的にアクセスできるようになるからだ。このページの実行フローは単純で、テキストボックスの値を設定し、ボタンをクリックするだけだ。
最後に、ページの最終的な状態をテストする必要がある。つまり、ラベルにテキストボックスの値が割り当てられているかどうかを確認するわけだ。NUnitAspのWebAssertion
クラスには、ページが予想どおりの状態になっているかどうかを確認するための標準メソッドが用意されている。このサンプルでは、ラベルが指定の値に等しいかどうかを確認したいので、AssertEquals
メソッドを使用する。
では、NUnitコンソールを開き、「UitGui.dll」をロードし、テストを実行してみよう。テストの横に緑の丸が表示されるはずだ。これは、テストに合格したことを意味する。
これで最初のテストは完了である。NUnitAspを使えば簡単だが、NUnitAspを使わなければもっと手間がかかったことだろう。
「Hello, World!」的なサンプルも悪くはないが、実際に解決しなければいけない問題はもっとずっと複雑である。以降では、NUnitAspを使ってこうした問題に取り組むため5つのテクニックを紹介する。
- 共通機能を基底クラスに抽象化する
- ラッパーページを使用する
- それぞれのテストを確実にベースラインにリセットする
- 単純な統合テストと機能テストを作成する
- テストをVisual Studioデバッガ内で実行する
共通機能を基底クラスに抽象化する
Browser.GetPage()
を呼び出した後は、Webアプリケーションが別のページ(ページのロードが失敗した場合のグローバルエラーページなど)にリダイレクトしていないかどうかをチェックする必要がある。このチェックは次のコードで実行できる。
WebAssertion.AssertEquals(Browser.CurrentUrl.ToString(),strURL);
しかし、これはどんなテストでも必要な処理なので、共通の基底ページオブジェクトに抽象化しておいた方が便利である。
public class TestBase : WebFormTestCase { public TestBase() { } //end of con public void CheckPage(string strExpectedUrl) { WebAssertion.AssertEquals("Expected page does not match actual page. " + "Expected=[" + strExpectedUrl + "]. Actual=[" + Browser.CurrentUrl.ToString() + "].", Browser.CurrentUrl.ToString(),strExpectedUrl); } //end of method } //end of class
この基底ページがWebFormTestCase
を継承していることに注目してほしい。ここでCheckPage()
メソッドを作成しておくと、このメソッドをすべてのテストから使用できるようになる。この基底クラスを実装したので、前述のテストクラスの宣言を、
[TestFixture] public class TestBasic : WebFormTestCase
から
[TestFixture] public class TestBasic : TestBase
に修正する必要がある。
ラッパーページを使用する
NUnitAspが直接アクセスできるのはWebFormだけである。ユーザーコントロールや、Session値などの項目を含んでいるHttpContext
に直接アクセスすることはできない。しかし、これらの要素をヘルパーのWebFormでラップすれば、NUnitAspから間接的に参照することが可能である。例えば、ドロップダウンリストとラベル(いずれもWebコントロール)を含んだUCDropDown.ascx
というユーザーコントロールをテストしたい場合だ。このユーザーコントロールには、次のメソッドとプロパティが含まれているものとする。
public string Subject public void SetValues(string[] astrValues) public string GetSelectedValue()
ここではTestUCDropDown.aspx
というWebFormを作成してテスト対象のユーザーコントロールを配置し、このユーザーコントロールのプロパティにアクセスするためのコントロールをいくつか追加する。つまり、ユーザーコントロールのSubject
プロパティの単体テストを作成するために、UCDropDown.ascx
ユーザーコントロールだけでなくテキストボックス、ラベル、ボタンも含んだTestUCDropDown.aspx
WebFormを作成するわけだ。
ボタンがクリックされると、テキストボックスの値がユーザーコントロールのSubject
プロパティに割り当てられる。ラベルには、Subject
プロパティの値が表示される。NUnitAspではページ上のWebコントロールを簡単に操作できるので、これらのWebコントロールを使ってユーザーコントロールを間接的に操作できる。次の図は、サンプルのWebFormを実行した様子である。
これはテスト時には大いに役立つが、このようなページを本番用のコードに含めるわけにはいかない。この問題を解決するには、ラッパーのWebFormを#if DEBUG
指示子と#endif
指示子で囲めばよい。そうすれば、この部分がDEBUGコードにのみ含まれ、リリースバージョンには含まれなくなる。例えば、分離コードの先頭に#if DEBUG
を記述し、末尾に#endif
を記述した場合は、そのページ全体がデバッグ時にのみ使用されるようになる。次のコードは、この方法を実際に採用した例である。
#if DEBUG using ... namespace WebMain { public class TestUCDropDown : System.Web.UI.Page { ... } public string Subject { get {return this.LblSubject.Text;} set{this.LblSubject.Text = value;} } } #endif
このテクニックを使用すると、ユーザーコントロールをテストするだけでなく、HttpContext
ユーティリティクラスの処理と、HttpContext
オブジェクトそのものの操作も行うことができる。例えば、クエリ文字列を検証するためのユーティリティクラスがある場合や、Session値およびキャッシュ値を操作しなければならない場合は、それらをWebFormでラップしてWebFormのコントロールを通じてそれらにアクセスし、NUnitAspを使って単体テストをビルドすればよい。サンプルコードのWrapper
フォルダにはいくつかの例があるので参考にしてもらいたい。
それぞれのテストを確実にベースラインにリセットする
テストの基本原則は、すべてのテストを同じベースラインから開始し、1つのテストが別のテストに影響を与えないようにすることである。Webアプリケーションの場合、これは、1つのテストで生成されたApplication値、Session値、キャッシュ値を別のテストに持ち込んではならないということを意味する。
状態をリセットするメソッドを呼び出すには、前述のテクニックで説明したとおり、ラッパーページを使用する。その他に、少々手間はかかるがより厳密な方法として、コマンドラインからiisreset
を実行してIISをリセットするというものがある(これにより、アプリケーション全体の状態がリセットされる)。このコマンドは、System.Diagnostics.Process
クラスを使って次のようにプログラム的に呼び出すことができる。
public void IISReset() { string strFile = "IISRESET"; ProcessStartInfo psi = new ProcessStartInfo(); psi.WindowStyle = ProcessWindowStyle.Hidden; psi.FileName = strFile; Process p = System.Diagnostics.Process.Start(psi); p.WaitForExit(); }
キャッシュ値とSession値をリセットする他に、現在のスレッドのCultureとUICultureもリセットしなければならないことがある。グローバライゼーションのために、現在のスレッドには、日付と通貨の書式を決定するCultureと、使用するリソースファイルを決定するUICultureという値がある。グローバルアプリケーションをテストする場合には、各テストの後に現在のスレッドを次のように設定して、これらの値を確実にリセットするべきである。
public void ResetCulture() { System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US"); System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US"); }
どのテストをリセットするかに応じて、これらのメソッドをテストから個別に呼び出すことも、クラスまたは基底クラスのTearDown()
メソッド内から呼び出すこともできる(グローバルスコープの場合)。
単純な統合テストと機能テストを作成する
動的に追加されるコントロールや構成可能なメニューユーザーコントロールのテストのように、厳密な意味でのGUI単体テストも確かに存在する。しかし、N層アプリケーションではプレゼンテーション層とそれ以下のすべての層が統合されていることがよくあるので、GUIテストを統合テストや機能テストと関連付けることが簡単にできる。NUnitAspでは、プレゼンテーション層を制御することにより、単純な機能テストや統合テストを暗黙的に記述することもできる。機能テストの例としては、ビジネスプロセスのフロー全体にわたってユーザーのアクションを調整することが考えられる。統合テストの例としては、単純にページを正しくロードすることが考えられる(これにより、アプリケーションがすべての層を正しく通ってデータベースに到達し、再び戻ってきたことが確認できる)。
サンプルコードのFlow
フォルダには、このようなテストのサンプルが収録されている。このサンプルのページ(PageA
)では、ドロップダウンから動物を選択するよう要求される。実際に選択すると、確認用のページ(PageB
)に進み、選択した動物の名前が表示される。このユーザーが行う1つ1つの手順を真似たテストを作成することにより、機能テストと統合テストを実現できる。これは簡単なサンプルだが、この考え方は複雑なビジネスプロセスにも応用できる。
このようなテストは、強力(かつ高価)なサードパーティ製ツールの代わりにはならないが、簡単にテストをしたいときには大いに役立つし、予算がない(または専門のテスト担当者がいない)開発チームでも手軽にテストを実施できるというメリットがある。
テストをVisual Studioデバッガ内で実行する
Visual Studio .NETデバッガの長所は、デバッガ内でコードのステップ実行やテストができることである。しかしNUnitAspでは、Webアプリケーションへのアクセス方法の都合により、デバッガでステップ実行をするとAspTester
オブジェクトがnull
になってしまう。1つの解決方法は、フリーのTestDriven.NETアドインをダウンロードすることだ。このアドインを使用すると、メソッド内にカーソルを置いて右クリックするだけで、そのメソッドのデバッグモードに入ることができる。
まとめ
本稿では、NUnitAspを使用してGUIコンポーネントの単体テストを実現する方法と、簡単なものであれば機能テストや統合テストも実現できるということを紹介した。NUnitAspには、その他にも次のような長所がある。
- 無料のツールである
- 既に開発者にお馴染みのNUnitに直接統合されている
- 使い方がわかりやすい。WebFormを制御するためのクラスライブラリを提供しているだけなので、そのライブラリを開発者の好きな.NET言語でプログラミングでき、新しいテストアプリケーションの使い方を覚える必要がない
- 数行のコードを書くだけで手軽にテストを作成できる
- オープンソースツールなので、サードパーティ製のライセンス製品よりも速いペースで機能向上や機能拡張が期待できる
NUnitAspの最大の短所は、Webアプリケーション内の分離コードクラスしかテストできないことだろう。つまり、JavaScriptなどのクライアントサイドコードをテストすることはできない。また、NUnitAspでは、単にNetscapeやIEで表示できるHTMLページではなく、適切な形式のHTMLページが要求される。HTMLページの有効性は、http://validator.w3.org/のオンライン検証プログラムで確認できる。
NUnitAspは便利なツールであり、特にGUIの単体テストには威力を発揮する。しかし、これを使用したからといって、その他の部分のテストプロセスが不要になるわけではないし、GUIからしかテストできないようなコードを書いてもよいことにはならない。バックエンドのビジネス層やデータベース層をテストするには、NUnitのような、別のテストフレームワークを使用する必要がある(NUnitの詳細については、「Test Driven Development Using NUnit in C#」を参照)。NUnitAspは無料で使い方も簡単なので、.NETツールキットに加えておくと重宝するプログラムである。
参考資料
- NUnitAspのWebサイト(バージョン1.5.1)
- NUnitのWebサイトhttp://www.nunit.org/(バージョン2.2)
- TestDriven.NETのWebサイト
- オンラインHTML検証ツール
- Test Driven Development in Microsoft.Net, James W. NewKirk and Alexei A. Vorontsov
- Pragmatic Unit Testing in C# with NUnit, Andrew Hunt and David Thomas