対ASP.NET MVCプロジェクトの中のビューはユニットテストをします
13913 ワード
対ASP.NET MVCプロジェクトの中のビューはユニットテストをします
2009-02-25 01:01 Jeffrey Zhao阅读(...)コメント(…)コレクションの編集
ビューのセルテストについて
ASPといえばNET MVCでは、Controllerのテストに常に注目しているようです.Stephen WaltherもWebサーバから離れてViewをユニットテストする方法を書いたことがありますが、彼の方法は見ることができますが、使用できません.複雑な構造と準備、生成されたHTML文字列の判断--これは本当にビューをユニットテストしているのだろうか.彼のコードをよく分析すると、これは実際にViewEngineに対してユニットテストをしていることがわかります.また、本当にViewEngineに対してユニットテストをするなら、彼のように外部ファイルに依存するべきではありません.私から見れば、彼のやり方は何でもない......美しく、いくつかの「拍手」を博できるようだが、この拍手は彼の解決策から来たのか、それともみんなの一時的な衝動から来たのか.
ビューをユニットテストする場合は、ブラウザにコンテンツを表示する必要があります.Webページのユニットテストでは、一般的にWatiNなどのツールを使用してブラウザを操作し、ページを開き、DOM要素の構造や内容を断言します.でも...これはユニットテストですか?残念ながら、これは回帰テストまたはユーザー検収テストとしか言えません.なぜなら、私たちはページを開くとき、表現層からビジネスロジック、データアクセスまで、アプリケーションの各部品が忙しいからです.ユニットテストは「分離」にこだわり、すべての関心を分離し、すべての依存を分離します.分離のため、私たちは正確に誤りを特定することができます.分離のため、私たちが準備したデータをテストで使用することができます.
分離する以上、私たちは一定の使用規範に従わなければなりません.『ASP.NET MVCユニットテストベストプラクティス』では、他のコンテンツ(HttpContextを含む)に依存するのではなく、ViewではViewデータのデータしか使用できないと述べています.これにより、Viewデータを独自に構築し、ビューオブジェクトに注入することができます.実は、この約束はASP.NET MVCが持参したプロジェクトテンプレートで破壊されました.ViewsSharedLogOnUserControlを見てください.ascx、ここでthis.ユーザーは、現在のユーザーのログインステータスを表示します.これは、現在のHttpContextから直接取得される、従来のPageオブジェクトに定義されたプロパティです.このようにすれば,セルテスト時に現在のユーザのログイン状態を「シミュレーション」することが難しく,テストをテストの様々な状況に上書きすることが困難になる.
Lightweight Test Automation Framework
ここで趙さんはASPをお勧めします.NET Teamが提供するLightweight Test Automation Framework(以下、LTAFという)は、現在CodePlexでFeb Updateバージョンに更新されているテストツールとして提供されています.このフレームワークの役割はWatiNやSeleniumと同様で、ブラウザを操作してアプリケーションに対して回帰テストを記述することができます.DOM要素の選択など、いくつかの点では「競合他社」に及ばないが、LTAFには独自の点がある.
ここで趙さんはASPをお勧めします.NET Teamが提供するLightweight Test Automation Framework(以下、LTAFという)は、現在CodePlexでFeb Updateバージョンに更新されているテストツールとして提供されています.このフレームワークの役割はWatiNやSeleniumと同様で、ブラウザを操作してアプリケーションに対して回帰テストを記述することができます.DOM要素の選択など、いくつかの点では「競合他社」に及ばないが、LTAFには独自の点がある.
第1点の優位性はもちろん、第2点はさらに重要だ.WatiNもSeleniumも、コードを書くことでブラウザでページを開くと思います.これは、テストコードとテストされたページがそれぞれ異なるプロセスにあることを意味します.この前提の下で、テストコードで定義されたデータをテストされたWebページ(すなわち、ビューオブジェクト)に渡す場合は、プロセス間通信を行う必要があります.どのように実現しても、「シーケンス化」から逃れられないのは、複雑さを増すに違いない.LTAFを使用すると、この問題は瞬時に消え去った.メモリにテストデータを直接「伝達」することができるので、すべては参照にすぎない.
しかし、何事にも両面性があり、LTAFにも生まれつきの欠点があり、永遠に補うことができない欠点がある.例:
しかし、幸いなことに、この2つの点は深刻な問題になっていません.1つ目は、windowsへの直接アクセスを置き換えるための独自のgetTopメソッドを作成する必要があります.トップのやり方でいいです.第2のケース--趙さんはalertやconfirmという「純粋なブラウザ機能」が好きではありません.それは悪いユーザー体験をもたらすからです.まして今のJavaScriptクラスライブラリ/フレームワークは簡単にこのような効果を出すことができます.どう思いますか.
LTAFの具体的な使用方法は、そのRelease Noteを参照することができる.不思議なことに、趙さんはプロジェクトでLTAFを直接使用するといくつかの小さな問題があることに気づきました(しかし、その例はなぜすべて正常なのでしょうか).そのため、いくつかの細かい修正が行われました.注意してください~UnitViewDriverPage.aspxファイルの末尾にあるJavaScriptコード.
UnitViewの使用
そこで趙さんはユニットテストに必要なデータを構築するのに便利なコンポーネントUnitViewを作成しました.データがあれば、ブラウザに直接ビューを表示できます.例:[WebTestClass]
public class HomeTests
{
[WebTestMethod]
public void LoggedOnIndexTest()
{
var data = new TestViewData<IndexModel>
{
ControllerName = "Home",
ActionName = "Index",
Model = new IndexModel
{
Message = "Welcome guys!",
Identity = new UserIdentity
{
IsAuthenticated = true,
Name = "Jeffrey Zhao"
}
}
};
HtmlPage page = new HtmlPage(TestViewData.GenerateHostUrl(data));
// Assert title
Assert.AreEqual("Home Page", page.Elements.Find("title", 0).GetInnerText());
// Assert head element
var mainContent = page.Elements.Find("main");
var head2 = mainContent.ChildElements.FindAll("h2").Single();
Assert.AreEqual(data.Model.Message, head2.GetInnerText(), "Message should be displayed.");
var loginTabInnerText = page.Elements.Find("logindisplay").GetInnerTextRecursively();
Assert.IsTrue(loginTabInnerText.Contains("Welcome"), "'Welcome' missed.");
Assert.IsTrue(loginTabInnerText.Contains(data.Model.Identity.Name), "Login name missed.");
}
}
もちろん、Webサーバは欠かせません.幸いなことに、分離により、ビューは最も簡単なテストデータにのみ関連し、VS独自の簡単なWebサーバで十分です.上記のコードでは、ビューを表示するために必要なすべてのデータを含む強力なタイプのTestViewオブジェクトを直接構築しました.
[WebTestClass]
public class HomeTests
{
[WebTestMethod]
public void LoggedOnIndexTest()
{
var data = new TestViewData<IndexModel>
{
ControllerName = "Home",
ActionName = "Index",
Model = new IndexModel
{
Message = "Welcome guys!",
Identity = new UserIdentity
{
IsAuthenticated = true,
Name = "Jeffrey Zhao"
}
}
};
HtmlPage page = new HtmlPage(TestViewData.GenerateHostUrl(data));
// Assert title
Assert.AreEqual("Home Page", page.Elements.Find("title", 0).GetInnerText());
// Assert head element
var mainContent = page.Elements.Find("main");
var head2 = mainContent.ChildElements.FindAll("h2").Single();
Assert.AreEqual(data.Model.Message, head2.GetInnerText(), "Message should be displayed.");
var loginTabInnerText = page.Elements.Find("logindisplay").GetInnerTextRecursively();
Assert.IsTrue(loginTabInnerText.Contains("Welcome"), "'Welcome' missed.");
Assert.IsTrue(loginTabInnerText.Contains(data.Model.Identity.Name), "Login name missed.");
}
}
TestViewData.GenerateHostUrlメソッドはdataを保存し、URLを返します.このURLにアクセスすると、対応するビューコンテンツが得られます.
UnitViewを使用する場合は、上記のリンクからUnitViewのソースコードとサンプルをダウンロードして、ネイティブで試してみてください.UnitViewを使用する場合、主に次の点に注意してください.
UnitView実装分析
UnitViewコンポーネントは非常に簡単で、簡単でほとんど言う価値がありません.TestViewDataタイプには、テストに必要なすべてのデータが含まれていますが、TestViewDataはTestViewDataを継承し、強力なタイプのModel属性アクセスを提供します.それらは分析しません.
さらに、TestViewDataにはいくつかの静的な方法があります.public class TestViewData
{
static TestViewData()
{
PersistentProvider = new InProcPersistentProvider();
}
public static IPersistentProvider PersistentProvider { get; set; }
public static string GenerateHostUrl(TestViewData data)
{
var key = PersistentProvider.Save(data);
return ViewHostHandlerUrl + "?key=" + HttpUtility.UrlEncode(key);
}
private static string ViewHostHandlerUrl
{
get
{
return ConfigurationManager.AppSettings["UnitView_ViewHostHandlerUrl"]
?? "/UnitView/ViewHostHandler.ashx";
}
}
internal static TestViewData Load(string key)
{
return PersistentProvider.Load(key);
}
...
}
GenerateHostUrlメソッドは、PersistentProviderにオブジェクトの保存を依頼し、keyを取得します.このキーは、ViewHostHandlerUrlプロパティに接続されます.これがテストされたパスです.コードから分かるように、デフォルトのテストパスを使用したくない場合は、web.configのAppSettingsノードにターゲットアドレスを追加すればよい.
PersistentProviderプロパティはIPersistentProviderインタフェースタイプで、Save/Load/Removeの3つのメソッドが定義されています.IPersistentProviderは、プロジェクトに実装されているのは1つだけです.InProcPersistentProviderは、TestViewDataをメモリ内の辞書に格納します.この実装は、UnitViewをLTAFと組み合わせて動作させるのに十分である(LTAFの同プロセス特性が重要な役割を果たしている).ただし、WatiNなどの独立したプロセスのテストツールを使用する場合は、独自のIPersistentProviderタイプを実装する必要があります.たとえば、FilePersistentProviderを実装し、TestViewDataを外部ファイルにシーケンス化することで、適切なタイミングで取り戻すことができます.
もう一つ重要なタイプはUnitViewです.Engine.ViewHostHandler: public class ViewHostHandler : IHttpHandler
{
private HttpContext Context { get; set; }
public void ProcessRequest(HttpContext context)
{
this.Context = context;
ControllerContext controllerContext = new ControllerContext(
new HttpContextWrapper(context),
this.Data.RouteData,
new MockController());
new ViewResult
{
MasterName = this.Data.MasterName,
ViewName = this.Data.ViewName,
TempData = this.Data.TempData,
ViewData = this.Data.ViewData,
}.ExecuteResult(controllerContext);
}
private string Key
{
get
{
string key = this.Context.Request.QueryString["key"];
if (String.IsNullOrEmpty(key))
{
throw new ArgumentNullException("key");
}
return key;
}
}
private TestViewData m_data;
private TestViewData Data
{
get
{
if (this.m_data == null)
{
this.m_data = TestViewData.Load(this.Key);
if (this.m_data == null)
{
throw new ArgumentNullException("Cannot retrieve the data.");
}
}
return this.m_data;
}
}
public bool IsReusable { get { return false; } }
}
まず、ProcessRequestメソッドでTestViewDataを取り戻し、これらのデータに基づいてViewResultオブジェクトを構築し、最後にそのExecuteResultメソッドを実行してビューコンテンツを出力します.ExecuteRequestメソッドの必要性から、ControllerContextオブジェクトを構築する必要があります.つまり、ControllerオブジェクトとHttpContextのパッケージを提供する必要があります.コードから分かるように、ここでは最も簡単なデータを使用しています.ビューは「約束」を守っているため、ViewDataからデータを取得するだけなので、ControllerまたはHttpContextがどの値であるかにかかわらず、重要ではありません.
なぜ、HttpContextオブジェクトからビューにデータを取得させない「約束」があるのでしょうか.Mock 1つのHttpContextオブジェクトもそんなに難しくはありません(ここでは強力なMockフレームワークに感謝します).残念なことに、Mock後のHttpContextはシーケンス化が難しく、プロセス間通信の可能性がほとんどなくなり、WatiNとSeleniumを使用してテストを行う友人たちにとっては災難に違いない.考えてみると、趙さんはHttpContextへの支持を放棄することにした.
注1:現在UnitViewはASPに基づくNET MVC RC構築、RTMリリース後に必要な更新を行います.趙さんのこの文章とMSDNコードGalleryに管理されているコードに注目してください.http://code.msdn.microsoft.com/UnitView).
注2:「ASP.NET MVCユニットテストベストプラクティス」にはUnitViewコンポーネントも含まれていますが、実装は少し異なります.この記事を中心にしてください.
分類02.ASP.NET , 05. 実践最適化ラベルユニットテスト、MVC、ビュー
public class TestViewData
{
static TestViewData()
{
PersistentProvider = new InProcPersistentProvider();
}
public static IPersistentProvider PersistentProvider { get; set; }
public static string GenerateHostUrl(TestViewData data)
{
var key = PersistentProvider.Save(data);
return ViewHostHandlerUrl + "?key=" + HttpUtility.UrlEncode(key);
}
private static string ViewHostHandlerUrl
{
get
{
return ConfigurationManager.AppSettings["UnitView_ViewHostHandlerUrl"]
?? "/UnitView/ViewHostHandler.ashx";
}
}
internal static TestViewData Load(string key)
{
return PersistentProvider.Load(key);
}
...
}
public class ViewHostHandler : IHttpHandler
{
private HttpContext Context { get; set; }
public void ProcessRequest(HttpContext context)
{
this.Context = context;
ControllerContext controllerContext = new ControllerContext(
new HttpContextWrapper(context),
this.Data.RouteData,
new MockController());
new ViewResult
{
MasterName = this.Data.MasterName,
ViewName = this.Data.ViewName,
TempData = this.Data.TempData,
ViewData = this.Data.ViewData,
}.ExecuteResult(controllerContext);
}
private string Key
{
get
{
string key = this.Context.Request.QueryString["key"];
if (String.IsNullOrEmpty(key))
{
throw new ArgumentNullException("key");
}
return key;
}
}
private TestViewData m_data;
private TestViewData Data
{
get
{
if (this.m_data == null)
{
this.m_data = TestViewData.Load(this.Key);
if (this.m_data == null)
{
throw new ArgumentNullException("Cannot retrieve the data.");
}
}
return this.m_data;
}
}
public bool IsReusable { get { return false; } }
}