接續上篇 ASP.NET MVC Route Unit Test - Part.3 內容。
提到如何使用「MvcRouteTester」來測試 WebAPI 專案的 WebApiConfig + ApiController。
依照預設方式,都將 Route 規則寫在 WebApiConfig 統一管理。但當系統複雜時,設定相對肥大,所以有時我們就希望用到 Attribute Routing。
接下來這篇紀錄 WebAPI 2 與 MVC 5 兩種專案,如何在使用 Attribute Routing 時,進行相關 Route 測試開發。
前言
關於「MvcRouteTester」元件的功能介紹,這裡就不贅述了 (可以參考前幾篇內容)
這裡就直接針對 Attribute Routing 開始測試說明。
上半部:純 WebAPI 2 專案的 Attribute Routing 測試。
下半部:純 MVC 5 專案的 Attribute Routing 測試。
紀錄
WebAPI 2
在上一篇內容中可以看到 WebApiConfig,有針對 ImagesController 加註了 MapImages(config) 的設定。
測試專案在偵錯模式下,可以察看到目前 HttpConfiguration 所有的 Route 設定。如下:
如果這時刻意把 MapImages(config) 註解,不用想,測試結果,一定賞一個「紅燈」
那麼這時偵錯來看看 HttpConfiguration,可以明確地看到少了 Images 相關的 Route 設定。
接下來會以另外一支程式來進行操作:
這時候開啟 Sample.WebAPI 新建一支 ArticlesController,這回就不到 WebApiConfig 設定了,直接在 Controller 加工...
using Sample.WebAPI.Models;
using System;
using System.Web.Http;
namespace Sample.WebAPI.Controllers
{
[RoutePrefix("API/Articles")]
public class ArticlesController : ApiController
{
[HttpGet]
[Route("{id}")]
public ArticleDataViewModel Get(string id)
{
var result = new ArticleDataViewModel
{
Title = "Article Test From API",
Content = "Good Article",
CreateAt = DateTime.UtcNow
};
return result;
}
}
}
我們在 ArticlesController 加上
[RoutePrefix("API/Articles")]
在 Get Action 加上 (在參數:id 之前,刻意不加 Method,希望 api/Articles/1 就直接帶入 id 抓取資料就好 )
[Route("{id}")]
那麼跑測試的結果呢 ? 就成功啦,「綠燈」如下:
OK,這樣上述的情況,就達成我們想要測試 WebAPI 2 專案 Attribute Routing 的目的。
接下來,我們就來嘗試來實做 MVC 5 專案 Attribute Routing 測試。
MVC 5
如果是 MVC 專案要使用 Attribute Routing,可以在 RouteConfig 加上
routes.MapMvcAttributeRoutes();
接著在 MVC 專案 Sample.MVC 也新增 ArticlesController,直接使用 Index Action,如下:
using Sample.MVC.Models;
using System;
using System.Web.Mvc;
namespace Sample.MVC.Controllers
{
[RoutePrefix("AllArticles")]
public class ArticlesController : Controller
{
[Route("{id}")]
public ActionResult Index(string id)
{
var result = new ArticleDataViewModel
{
Title = "Article Test From MVC",
Content = "Good Article",
CreateAt = DateTime.UtcNow
};
return Json(result, JsonRequestBehavior.AllowGet);
}
}
}
[RoutePrefix("AllArticles")]
[Route("{id}")]
在上述設定完成之後,當然順勢加上 ArticlesMvcRouteTest 單元測試程式。如下:
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcRouteTester;
using Sample.MVC;
using Sample.MVC.Controllers;
using System;
using System.Net.Http;
using System.Web.Routing;
namespace Use_MvcRouteTester_To_TestRoute.Routes
{
[TestClass]
public class ArticlesMvcRouteTest : IDisposable
{
private RouteCollection testRoutes;
public ArticlesMvcRouteTest()
{
//// Arrange
testRoutes = new RouteCollection();
RouteConfig.RegisterRoutes(testRoutes);
}
public void Dispose()
{
testRoutes.Clear();
}
[TestMethod]
public void ArticlesRoute_WithHttpMethod_Get_RouteWith_Controller_Get_Action_Id_ShouldMap()
{
testRoutes.ShouldMap("/AllArticles/1")
.To<ArticlesController>(c => c.Index(string.Empty));
testRoutes.ShouldMap("/AllArticles/1")
.To<ArticlesController>(HttpMethod.Get, c => c.Index(string.Empty));
}
}
}
直接執行所有測試,結果...
發生 System.InvalidOperationException 非預期的錯誤。
結果 StackTrace:
於 System.Web.Compilation.BuildManager.EnsureTopLevelFilesCompiled()
於 System.Web.Compilation.BuildManager.GetReferencedAssemblies()
於 System.Web.Mvc.BuildManagerWrapper.System.Web.Mvc.IBuildManager.GetReferencedAssemblies()
於 System.Web.Mvc.TypeCacheUtil.FilterTypesInAssemblies(IBuildManager buildManager, Predicate`1 predicate)
於 System.Web.Mvc.TypeCacheUtil.GetFilteredTypesFromAssemblies(String cacheName, Predicate`1 predicate, IBuildManager buildManager)
於 System.Web.Mvc.ControllerTypeCache.EnsureInitialized(IBuildManager buildManager)
於 System.Web.Mvc.DefaultControllerFactory.GetControllerTypes()
於 System.Web.Mvc.Routing.AttributeRoutingMapper.MapAttributeRoutes(RouteCollection routes, IInlineConstraintResolver constraintResolver, IDirectRouteProvider directRouteProvider)
於 System.Web.Mvc.Routing.AttributeRoutingMapper.MapAttributeRoutes(RouteCollection routes, IInlineConstraintResolver constraintResolver)
於 System.Web.Mvc.RouteCollectionAttributeRoutingExtensions.MapMvcAttributeRoutes(RouteCollection routes)
於 Sample.MVC.RouteConfig.RegisterRoutes(RouteCollection routes) 於 E:\DevProjects\Dev2015\UnitTest-MVC-Route\Sample.MVC\App_Start\RouteConfig.cs: 行 12
於 Use_MvcRouteTester_To_TestRoute.Routes.ImagesMvcRouteTest..ctor() 於 E:\DevProjects\Dev2015\UnitTest-MVC-Route\Use-MvcRouteTester-To-TestRoute\Routes\ImagesMvcRouteTest.cs: 行 20
結果訊息: 無法建立類別 Use_MvcRouteTester_To_TestRoute.Routes.ImagesMvcRouteTest 的執行個體。錯誤: System.InvalidOperationException: 這個方法不可在應用程式的啟動前初始設定階段呼叫。
果然「代誌不是憨人想的那麼簡單 !」
這時想到試著將剛剛加在 RouteConfig 的 routes.MapMvcAttributeRoutes(); 程式移除。
改成到 MVC 專案的 Global.asax 加上 Attribute Routes 的註冊,再跑一次所有測試
RouteTable.Routes.MapMvcAttributeRoutes();
這時可以看到,只剩下我們有設定 Attribute Routing 的 MVC ArticlesController 的案例出錯,而且明確地指出 Route 比對失敗。
接下來看怎麼做才會通過了,查了一下「MvcRouteTester - Issue」#29 作者依據提問,調整 MapAttributeRoutes 測試的方法。(commit)
依據這個方式,就調整一下 ArticlesMvcRouteTest 測試初始化的程式
public ArticlesMvcRouteTest()
{
//// Arrange
testRoutes = new RouteCollection();
testRoutes.MapAttributeRoutesInAssembly(typeof(ArticlesController));
RouteConfig.RegisterRoutes(testRoutes);
}
終於「綠燈」啦...而且在偵錯模式下確認 RouteCollection 內容,也能看到加上的 Attribute Routing 設定。
以上就是針對 WebAPI 2 + MVC 5 專案的 Attribute Routing 新增的測試案例。
最後做個記錄,在 MVC 的測試中有個不嚴謹的點,關於 MapAttributeRoutesInAssembly 的物件參數。
MapAttributeRoutesInAssembly(typeof(ArticlesController));
如果將 ArticlesController 隨意改成 ImagesController
MapAttributeRoutesInAssembly(typeof(ImagesController));
卻能正常抓到 ArticleController 的 Attribute Routing 設定,且測試案例也通過。這結果跟期望不同。
有可能自己不熟悉;理解錯誤。也會在這幾天爬文 or 發 GitHub Issue 向作者確認。
總結
透過 Attribute Routing 設定,程式變得相當簡潔 + 保持彈性、靈活。
搭配「MvcRouteTester」元件來實做 Route 測試,除了 MVC RouteConfig + WebAPI WebApiConfig 的設定外,我們還能夠一起測試到 Attribute Routing;讓保護更加全面性。
而上述的 Attribute Routing 相當單純,若大家有其他複雜的設定,歡迎提出,我也來測試看看,最後若大家在實務上使用起來有什麼疑問,歡迎一起討論。