在上一篇文章(使用Asp.Net MVC 動態產生View Part.2 - 使用IViewEngine、IView)中,利用了IViewEngine, IView完成了動態頁面的產生。然而,動態產生頁面的功能是非常需要在每次上版前執行測試,這樣的功能當出現bug時,在第一時間就會被使用者發現,所以必須確保每次上版時,動態產生頁面的功能是正確的。
在本篇文章中,將分別針對單元測試(針對Action)與整合測試兩個部份說明。另外,範例將會使用
- VS2015的擴充套件 - Specflow for Visual Studio 2015
- Nuget套件 - Specflow.MsTest 2.0.0
- Nuget套件 - NSubstitute 1.9.2
在這裡將不會特別說明上述套件如何使用,如果有需要Specflow教學可以到91哥的點部落文章有詳細說明,另一個套件Nsubstitute可至官網有詳細說明。
單元測試
單元測試的部分,這邊只針對Controller的Action進行測試,其他部分將不在此討論說明。
在開始寫測試前,需先將上一篇文章中的範例把Action以外用到HtmlGeneratorHelper的地方改為由外部注入,除Controller以外的類別不與HtmlGeneratorHelper產生耦合。修改的部分為
DynamicViewController.cs
public class DynamicViewController : Controller
{
public IHtmlGeneratorHelper HtmlGeneratorHelper;
public DynamicViewController()
{
this.HtmlGeneratorHelper = new HtmlGeneratorHelper();
}
public ActionResult GetView(string pageName, string id)
{
//// remove default view engine
this.ViewEngineCollection.Clear();
//// add custom view engine
this.ViewEngineCollection.Add(new CustomViewEngine(this.HtmlGeneratorHelper));
var view = string.Format("{0}-{1}", id, pageName);
return View(view);
}
}
CustomViewEngine.cs
private IHtmlGeneratorHelper _htmlGenerator;
public CustomViewEngine(IHtmlGeneratorHelper htmlGeneratorHelper)
{
this._htmlGenerator = htmlGeneratorHelper;
}
/// <summary>
/// Fined view
/// </summary>
/// <param name="controllerContext">controller context</param>
/// <param name="viewName">view name</param>
/// <param name="masterName">layout view</param>
/// <param name="useCache">use chace</param>
/// <returns>ViewEngineResult</returns>
public ViewEngineResult FindView(ControllerContext controllerContext,
string viewName, string masterName, bool useCache)
{
var viewObj = SplitViewName(viewName);
return new ViewEngineResult(new CustomView(viewObj.Item2, viewObj.Item1, this._htmlGenerator), this);
}
/// <summary>
/// Find partial view
/// </summary>
/// <param name="controllerContext">controller context</param>
/// <param name="partialViewName">partial view name</param>
/// <param name="useCache">use cache</param>
/// <returns>ViewEngineResult</returns>
public ViewEngineResult FindPartialView(ControllerContext controllerContext,
string partialViewName, bool useCache)
{
var viewObj = SplitViewName(partialViewName);
return new ViewEngineResult(new CustomView(viewObj.Item2, viewObj.Item1, this._htmlGenerator), this);
}
另外,為了讓單元測試與整合測試能夠用同一組瀏覽頁面時所傳入的參數,需要將HtmlGeneratorHelper.cs也稍微修改一下
public string GetHtml(string pageName, string id)
{
StringBuilder sb = new StringBuilder();
var content = string.Empty;
//// 根據pagename給出不同的content
content = string.Format("<div>I am {0}</div><div>PageName : {1}</div>", id, pageName);
sb.AppendLine(content);
return sb.ToString();
}
測試案例
修改好上一篇文章的範例後,就可以開始寫測試案例。動態頁面的需求中,我們希望能夠透過ID以及PageName來取得使用者編輯產生的動態頁面HTML,所以測試案例會像這樣
步驟一 : 先設定使用者編輯好的HTML
步驟二 : 瀏覽頁面時所傳入的參數(ID, PageName)
步驟三 : 呼叫Action
步驟四 : 驗證實際產出的HTML與期望的HTML是否相同
測試程式
寫好測試案例後,就可以開始撰寫測試程式碼,利用Specflow幫我們產出測試程式的骨幹後,就一塊一塊將程式碼寫入對應的區塊。由於是針對Controller的Action進行單元測試,所以必須產生一個假HtmlGeneratorHelper物件,當輸入的參數為測試案例中的ID與PageName時,會傳回指定的HTML。
設定已經編輯好的動態頁面HTML
[Given(@"使用者儲存專屬頁面HTML為")]
public void Given使用者儲存專屬頁面HTML為(Table table)
{
this._oriHtml = table.Rows[0]["HTML"];
}
設定傳入的參數,另外指定呼叫假HtmlGeneratorHelper物件的GetHtml方法時,如果傳入參數為"唐伯虎"、"9527",則回傳指定的HTML
[Given(@"瀏覽頁面時所傳入的參數")]
public void Given瀏覽頁面時所傳入的參數(Table table)
{
this._id = table.Rows[0]["ID"];
this._pageName = table.Rows[0]["PageName"];
var returnHtml = string.Format(this._oriHtml, this._id, this._pageName);
this._stubHtmlGeneratorHelper.GetHtml(this._pageName, this._id).Returns(returnHtml);
}
產生測試對象,並將target裡的HtmlGeneratorHelper設定為這邊產生的假物件
[When(@"傳回頁面時")]
public void When傳回頁面時()
{
var httpContext = GetHttpContext();
var target = ControllerFactory.CreateController<DynamicViewController>(httpContext);
var viewResult = target.GetView(this._pageName, this._id) as ViewResult;
target.HtmlGeneratorHelper = this._stubHtmlGeneratorHelper;
viewResult.ExecuteResult(target.ControllerContext);
this._actualViewHeml = httpContext.Response.Output.ToString();
//// remove \r\n
this._actualViewHeml = this._actualViewHeml.Replace(System.Environment.NewLine, string.Empty);
}
取得GetView的回傳物件後,將其轉型為ViewResult。可能有人會覺得奇怪,GetView的方法簽章上明明是回傳ActionResult,怎麼可以轉型為ViewResult?這是因為ViewResult繼承了ViewResultBase, ViewResultBase又繼承了ActionResult,所以可以轉型為ViewResult。
那為甚麼需要將回傳的物件轉型為ViewResult呢?因為GetView回傳的物件其實尚未Render出HTML,需要透過呼叫ViewResult的ExcuteResult才會Render出HTML並且存放到HttpContext.Response.Output中。最後在將HttpContext.Response.Output.ToString()即可取得頁面的HTML。
此外,在這邊有兩點需要特別說明
-
產生HttpContext的過程有許多屬性需要設定,所以將這些步驟獨立為一個方法
private HttpContext GetHttpContext() { var httpRequest = new HttpRequest("", "http://localhost/DynamicView/GetView", ""); var httpContext = new HttpContext(httpRequest, new HttpResponse(new StringWriter())); var userAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.10) " + "Gecko/20100914 Firefox/3.6.10"; var browser = new HttpBrowserCapabilities { Capabilities = new Hashtable { { string.Empty, userAgent } } }; var factory = new BrowserCapabilitiesFactory(); factory.ConfigureBrowserCapabilities(new NameValueCollection(), browser); httpContext.Request.Browser = browser; return httpContext; }
-
由於測試對象為Controller的Action,在產生controller步驟繁瑣,所以透過另一個類別方法將產生controller的過程封裝起來,只需要呼叫方法時將一個HttpContext傳入。
public static T CreateController<T>(HttpContext context, RouteData routeData = null) where T : Controller, new() { T controller = new T(); HttpContextBase wrapper = null; wrapper = new HttpContextWrapper(context); if (routeData == null) { routeData = new RouteData(); } // add the controller routing if not existing if (!routeData.Values.ContainsKey("controller") && !routeData.Values.ContainsKey("Controller")) { routeData.Values.Add("controller", controller.GetType().Name .ToLower() .Replace("controller", "")); } controller.ControllerContext = new ControllerContext(wrapper, routeData, controller); return controller; }
最後,比對實際產生的HTML與預期的HTML是否一致
[Then(@"該頁面的HTML為")]
public void Then該頁面的HTML為(Table table)
{
var expectedHtml = table.Rows[0]["HTML"];
Assert.AreEqual(expectedHtml, this._actualViewHeml);
}
實際執行測試,執行後得到綠燈
整合測試
整合測試中將不會使用任何假物件,完全使用Production code。
測試案例
整合測試的測試案例與單元測試雷同,唯一不一樣的是不需要假設使用者所編輯儲存的HTML
因為不在使用假的HtmlGeneratorHelper物件,在Production code裡HtmlGeneratorHelper會根據傳入的id、pagename參數回傳一組字串
<div>I am {id}</div><div>PageName : {pagename}</div>
所以在Then的部分的驗證字串是這樣來的。
測試程式
整合測試的程式碼與單元測試大致相同,但是不再需要產生假的HtmlGeneratorHelper物件。實際執行測試,執行後得到綠燈。
結論
在上述的說明中,介紹了如何針對產生動態頁面的功能進行單元測試與整合測試,這樣的方法不單單只是可以用在動態頁面,在一般Asp.net MVC中的View也可以進行這樣的測試。但是,有一點必須注意的是
執行View頁面的測試用意是什麼和想要測試什麼
為什麼這樣說呢?在動態頁面這樣的功能中,每次頁面HTML的產生都是由資料庫或是其他儲存方式中取得樣板,在套入ViewModel產生頁面的HTML,整個過程都是我們自己處理,所以這樣的方式需要在每次上版前都經過測試確認沒有問題。然而,Asp.net MVC原本的頁面產生方式,都是靠系統的RazorEngine產生,一般來說並不會修改到這個部分的程式碼,所以測試的必要性就沒有來得那麼重要。但是,並不是說使用Asp.net MVC原本的頁面產生方式就不需要測試,而是應該針對頁面上的某一段HTML、特定的功能或是複雜的頁面進行測試,而不是對整頁的HTML測試。
參考資料
免責聲明:
"文章一定有好壞,文章內容有對有錯,使用前應詳閱公開說明書"