Пользовательские ограничения маршрута ASP.NET MVC, внедрение зависимостей и модульное тестирование - PullRequest
4 голосов
/ 25 января 2012

Об этой теме я задал еще один вопрос:

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

Здесь я не могу понять, как передать зависимость.Есть идеи?

Ответы [ 3 ]

9 голосов
/ 26 января 2012

Лично я стараюсь избегать такой проверки в рамках ограничения маршрута, так как выразить свои намерения гораздо сложнее.Вместо этого я использую ограничения, чтобы гарантировать, что параметры находятся в правильном формате / типе, и помещаю такую ​​логику в мои контроллеры.

В вашем примере я предполагаю, что если страна недействительна, вы вернетесь к другоймаршрут (скажем, на страницу "Страна не найдена").Полагаться на свою конфигурацию маршрутизации гораздо менее надежно (и гораздо более вероятно, что она будет нарушена), чем принимать все параметры страны и проверять их в вашем контроллере:

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

На этот раз, когда мы запускаем наши тесты маршрутизациимы не тестируем нашу логику проверки или доступ к базе данных.Вместо этого мы проводим модульное тестирование нашей конфигурации маршрутизации, когда указана допустимая или недействительная страна.

1 голос
/ 25 января 2012

Теперь, учитывая предоставленную вами информацию, фактическая зависимость, о которой вы беспокоитесь, это зависимость от 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.Обычно это традиционная реализация Пурмана.Хотя это скрывает зависимость на DependencyResolver 1 шаг дальше, я чувствую, что это довольно хорошо.Это соответствует соглашению для DI Пурмана и даст вам более ожидаемое поведение.

Если бы у вас был класс, сконструированный таким образом, как выше, то, когда вы прошли тестовый модуль, вы, скорее всего, получили бы исключение, что DependencyResolver не знает, как активировать ICountryRepository<Country>, что подтолкнет вас в очевидном направлении исправления.Хотя я полагаю, что вы, вероятно, получили то же исключение, поскольку вы прямо назвали DependencyResolver, все равно очень шумно, что нужно писать DependencyResolver.Current.GetService<ICountryRepository<Country>>() более одного раза.

1 голос
/ 25 января 2012

Что вы тестируете?Мне кажется, что вам нужно только модульное тестирование вашего ограничения, а не механизм маршрутизации.В этом случае вам следует создать экземпляр своего ограничения и протестировать его метод Match.Как только вы узнаете, что ваше ограничение работает, вы можете провести некоторое ручное тестирование, чтобы убедиться, что ваш маршрут отображается правильно.Вероятно, это будет необходимо для обеспечения правильного упорядочения маршрутов, чтобы вы в любом случае не совпали слишком рано (или поздно) в наборе.

...