asp.net-mvc - ASP.NET MVC 自定义路由约束、依赖注入(inject)和单元测试

标签 asp.net-mvc asp.net-mvc-3 unit-testing dependency-injection moq

关于这个话题,我问了另一个问题:

ASP.NET MVC Custom Route Constraints and Dependency Injection

这是当前情况:在我的 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"]);
}

在这里,我不知道如何传递依赖关系。有什么想法吗?

最佳答案

就我个人而言,我尝试避免在路由约束内执行此类验证,因为以这种方式表达您的意图要困难得多。相反,我使用约束来确保参数采用正确的格式/类型,并将此类逻辑放入我的 Controller 中。

在您的示例中,我假设如果该国家/地区无效,那么您将退回到其他路线(例如“未找到国家/地区”页面)。与接受所有国家/地区参数并在 Controller 中检查它们相比,依赖路由配置的可靠性要低得多(而且更有可能被破坏):

    public ActionResult Country(string country)
    {
        if (country == "france") // lookup to db here
        {
            // valid
            return View();
        }

        // invalid 
        return RedirectToAction("NotFound");
    }

除此之外,您在这里试图实现的目标(正如已经提到的)实际上是集成测试。当您发现框架的某些部分妨碍了您的测试时,那么可能是时候进行重构了。在你的例子中我想测试

  1. 国家/地区已正确验证
  2. 我的路由配置。

我们能做的第一件事是将国家/地区验证移到一个单独的类中:

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 工具为您注入(inject)这些内容,则必须模拟您的验证器和存储库/数据库,并在测试设置中使用 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);
    }

这一次,当我们运行路由测试时,我们测试我们的验证逻辑或数据库访问。相反,当提供有效或无效的国家/地区时,我们将对路由配置进行单元测试。

关于asp.net-mvc - ASP.NET MVC 自定义路由约束、依赖注入(inject)和单元测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9003736/

相关文章:

css - 如何在 Squishit 中添加动态 CSS 源 URL?

asp.net-mvc - ASP MVC3在actionlink内插入html标记

javascript - 是否有像 Junit for Javascript 这样的框架来测试 Node.js 中的面向对象的 javascript?

javascript - 在 MVC 项目中访问 Javascript 内的服务器标签

asp.net-mvc - Asp.Net核心2.1 : Set partial view default location formats

asp.net-mvc - 将 IIS 7.5 预热模块与 ASP.NET MVC AuthorizeAttribute 结合使用

unit-testing - MR单元 : Tests fail with custom writable

unit-testing - 在 jestjs 中完成测试后无法登录

c# - 如何获取当前用户的SelectList

asp.net-mvc - MVC 角色授权