ASP.NET MVC 自定義路由約束、依賴注入和單元測試
關於這個話題,我問了另一個問題:
這是目前的情況:在我的 ASP.NET MVC 3 應用程序中,我定義了一個路由約束,如下所示:
public class CountryRouteConstraint : IRouteConstraint { private readonly ICountryRepository<Country> _countryRepo; public CountryRouteConstraint(ICountryRepository<Country> countryRepo) { _countryRepo = countryRepo; } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { //do the database look-up here //return the result according the value you got from DB return true; } }我正在使用它,如下所示:
routes.MapRoute( "Countries", "countries/{country}", new { controller = "Countries", action = "Index" }, new { country = new CountryRouteConstraint( DependencyResolver.Current.GetService<ICountryRepository<Country>>() ) } );在單元測試部分,我使用了以下程式碼:
[Fact] public void country_route_should_pass() { var mockContext = new Mock<HttpContextBase>(); mockContext.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath).Returns("~/countries/italy"); var routes = new RouteCollection(); TugberkUgurlu.ReservationHub.Web.Routes.RegisterRoutes(routes); RouteData routeData = routes.GetRouteData(mockContext.Object); Assert.NotNull(routeData); Assert.Equal("Countries", routeData.Values["controller"]); Assert.Equal("Index", routeData.Values["action"]); Assert.Equal("italy", routeData.Values["country"]); }在這裡,我無法弄清楚如何傳遞依賴關係。任何想法?
就我個人而言,我盡量避免在路由約束內執行此類驗證,因為以這種方式表達您的意圖要困難得多。相反,我使用約束來確保參數的格式/類型正確,並將此類邏輯放入我的控制器中。
在您的範例中,我假設如果該國家/地區無效,那麼您將退回到不同的路線(例如“未找到國家/地區”頁面)。與接受所有國家/地區參數並在控制器中檢查它們相比,依靠您的路由配置可靠性要差得多(並且更有可能被破壞):
public ActionResult Country(string country) { if (country == "france") // lookup to db here { // valid return View(); } // invalid return RedirectToAction("NotFound"); }除此之外,您在此處嘗試實現的目標(如前所述)實際上是集成測試。當您發現框架的某些部分妨礙了您的測試時,可能是重構的時候了。在您的範例中,我想測試
- 國家得到正確驗證
- 我的路由配置。
我們可以做的第一件事是將 Country 驗證移到一個單獨的類中:
public interface ICountryValidator { bool IsValid(string country); } public class CountryValidator : ICountryValidator { public bool IsValid(string country) { // you'll probably want to access your db here return true; } }然後我們可以將其作為一個單元進行測試:
[Test] public void Country_validator_test() { var validator = new CountryValidator(); // Valid Country Assert.IsTrue(validator.IsValid("france")); // Invalid Country Assert.IsFalse(validator.IsValid("england")); }我們的
CountryRouteConstraint然後更改為:public class CountryRouteConstraint : IRouteConstraint { private readonly ICountryValidator countryValidator; public CountryRouteConstraint(ICountryValidator countryValidator) { this.countryValidator = countryValidator; } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { object country = null; values.TryGetValue("country", out country); return countryValidator.IsValid(country as string); } }我們像這樣映射我們的路線:
routes.MapRoute( "Valid Country Route", "countries/{country}", new { controller = "Home", action = "Country" }, new { country = new CountryRouteConstraint(new CountryValidator()) });現在,如果您真的覺得有必要測試 RouteConstraint,您可以獨立測試:
[Test] public void RouteContraint_test() { var constraint = new CountryRouteConstraint(new CountryValidator()); var testRoute = new Route("countries/{country}", new RouteValueDictionary(new { controller = "Home", action = "Country" }), new RouteValueDictionary(new { country = constraint }), new MvcRouteHandler()); var match = constraint.Match(GetTestContext(), testRoute, "country", new RouteValueDictionary(new { country = "france" }), RouteDirection.IncomingRequest); Assert.IsTrue(match); }就我個人而言,我不會費心執行這個測試,因為我們已經抽象了驗證程式碼,所以這只是測試框架。
要測試路由映射,我們可以使用 MvcContrib 的TestHelper。
[Test] public void Valid_country_maps_to_country_route() { "~/countries/france".ShouldMapTo<HomeController>(x => x.Country("france")); } [Test] public void Invalid_country_falls_back_to_default_route() { "~/countries/england".ShouldMapTo<HomeController>(x => x.Index()); }根據我們的路由配置,我們可以驗證有效的國家/地區映射到國家/地區路由,而無效的國家/地區映射到備用路由。
但是,您問題的重點是如何處理路由約束的依賴關係。上面的測試實際上是在測試很多東西——我們的路由配置、路由約束、驗證器以及可能對儲存庫/數據庫的訪問。
如果您依賴 IoC 工具為您注入這些,您將不得不模擬您的驗證器和儲存庫/db,並在您的測試設置中使用您的 IoC 工具註冊它們。
如果我們可以控制約束的創建方式會更好:
public interface IRouteConstraintFactory { IRouteConstraint Create<TRouteConstraint>() where TRouteConstraint : IRouteConstraint; }您的“真實”實現可以只使用您的 IoC 工具來創建
IRouteConstraint實例。我喜歡將我的路由配置放在一個單獨的類中,如下所示:
public interface IRouteRegistry { void RegisterRoutes(RouteCollection routes); } public class MyRouteRegistry : IRouteRegistry { private readonly IRouteConstraintFactory routeConstraintFactory; public MyRouteRegistry(IRouteConstraintFactory routeConstraintFactory) { this.routeConstraintFactory = routeConstraintFactory; } public void RegisterRoutes(RouteCollection routes) { routes.MapRoute( "Valid Country", "countries/{country}", new { controller = "Home", action = "Country" }, new { country = routeConstraintFactory.Create<CountryRouteConstraint>() }); routes.MapRoute("Invalid Country", "countries/{country}", new { controller = "Home", action = "index" }); } }可以使用工廠創建具有外部依賴關係的約束。
這使得測試更容易。由於我們只對測試國家/地區路線感興趣,我們可以創建一個只做我們需要的測試工廠:
private class TestRouteConstraintFactory : IRouteConstraintFactory { public IRouteConstraint Create<TRouteConstraint>() where TRouteConstraint : IRouteConstraint { return new CountryRouteConstraint(new FakeCountryValidator()); } }請注意,這一次我們使用了一個
FakeCountryValidator包含足夠邏輯來測試我們的路由:public class FakeCountryValidator : ICountryValidator { public bool IsValid(string country) { return country.Equals("france", StringComparison.InvariantCultureIgnoreCase); } }當我們設置測試時,我們將 傳遞
TestRouteFactoryConstraint給我們的路由系統資料庫:[SetUp] public void SetUp() { new MyRouteRegistry(new TestRouteConstraintFactory()).RegisterRoutes(RouteTable.Routes); }這一次,當我們執行路由測試時,我們不是在測試我們的驗證邏輯或數據庫訪問。相反,我們在提供有效或無效國家/地區時對我們的路由配置進行單元測試。