ASP.NET MVC Route Unit Test - Part.4 - MvcRouteTester - Attribute Routing

接續上篇 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 測試。
範例專案:GitHub

紀錄

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 相當單純,若大家有其他複雜的設定,歡迎提出,我也來測試看看,最後若大家在實務上使用起來有什麼疑問,歡迎一起討論。

資源

●  MvcRouteTester GitHub
●  MvcRouteTester Wiki Usage