【问题标题】:ASP.NET MVC Custom Route Constraints, Dependency Injection and Unit TestingASP.NET MVC 自定义路由约束、依赖注入和单元测试
【发布时间】:2012-01-25 13:47:28
【问题描述】:

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

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"]);
}

在这里,我无法弄清楚如何传递依赖项。有什么想法吗?

【问题讨论】:

  • @ChrisMarisic ICountryRepository&lt;Country&gt; to CountryRouteConstraint 自定义路由约束
  • @ChrisMarisic 基本上,在我的单元测试项目中,我应该替换 country 路由约束。
  • TugberkUgurlu.ReservationHub.Web.Routes.RegisterRoutes(routes); 究竟是一个执行代码块routes.MapRoute("Countries",.... 的包装器?
  • @ChrisMarisic 完全正确。它与Global.asax.cs 中的默认值相同。

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


【解决方案1】:

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

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

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

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

除此之外,您在此处尝试实现的目标(如前所述)实际上是集成测试。当您发现框架的某些部分妨碍了您的测试时,可能是重构的时候了。在您的示例中,我想测试

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

我们可以做的第一件事是将 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);
    }

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

【讨论】:

  • 好的,这对我来说是最好的方式。我已经写了一个示例并且工作得很好。虽然需要重构,但乍一看看起来很棒。这里是:github.com/tugberkugurlu/RouteConstraintFactoryMvcApplication
  • @BenFoster,就我而言,我正在构建一个多租户应用程序,需要验证当前子域是否映射到数据库中的现有租户。我需要向IRoute/IRouteConstraint 注入服务,有没有其他方法可以考虑使用 DI 支持验证路由?
【解决方案2】:

你在测试什么?在我看来,您只需要对约束进行单元测试,而不是路由引擎。在这种情况下,您应该实例化您的约束并测试它的 Match 方法。一旦您知道您的约束有效,您就可以进行一些手动测试以确保您的路线映射正确。这可能是确保您的路线正确排序所必需的,这样您就不会在集合中太早(或太晚)匹配。

【讨论】:

  • 谢谢!我应该自动化路由的单元测试,我不是在这里测试路由逻辑。我正在测试以确保我有与预期相同的传入 url。基本上,在我的单元测试项目中,我应该替换 country 路由约束,但我不知道如何。
【解决方案3】:

现在根据您提供的信息,您关心的实际依赖关系是对DependencyResolver 的依赖关系(还有其他人发现其中有些讽刺吗?)。

你会想做类似的事情

var mockContext2 = new Mock<IDependencyResolver>();
    mockContext2.Setup(c => 
        c.GetService(It.Is.Any<ICountryRepository<Country>>())
    .Returns(____ whatever you want);

DependencyResolver.SetResolver(mockContext2.Object);

在您使用路由设置之前。

补充信息:

如果你改变了,你的代码可能会更干净

new CountryRouteConstraint(DependencyResolver.Current
                            .GetService<ICountryRepository<Country>>()

将其包含在类本身中

public CountryRouteConstraint() : 
    this(DependencyResolver.Current.GetService<ICountryRepository<Country>>()) {}

public CountryRouteConstraint(ICountryRepository<Country> repository) {}

然后您只需新建CountryRouteConstraint。这通常是Poorman DI 的传统实现。虽然它确实掩盖了对DependencyResolver 1 步的依赖,但我觉得这很好。它符合Poorman's DI 的惯例,它会给你更多的预期行为。

如果您按照上述方式构建类,那么当您进行单元测试时,您很可能会遇到 DependencyResolver 不知道如何激活 ICountryRepository&lt;Country&gt; 的异常,这会将您推向明显的方向解决这个问题。虽然我想你可能得到了同样的例外,因为你直接调用了DependencyResolver,但仍然需要多次写DependencyResolver.Current.GetService&lt;ICountryRepository&lt;Country&gt;&gt;()

【讨论】:

  • 我曾想过按照您的建议执行此操作,但有人建议我不应该在单元测试中使用 DependencyResolver。不过,我不知道为什么。
  • @tugberk 这是一个公平的说法,在这个单元测试中需要大量的配置,因为你必须伪造DependencyResolver,这使得这对纯粹主义者来说是一个真正的“集成”测试。您将如何以更可插拔的方式解决这种依赖关系,我不确定您会做什么。所以我觉得这些测试是完全可以接受的。
猜你喜欢
  • 1970-01-01
  • 2017-10-19
  • 2014-01-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-31
  • 2018-03-03
  • 2020-03-10
相关资源
最近更新 更多