Тестирование поведения маршрута
В предыдущем разделе вы видели, как можно случайно нарушить схему маршрутизации приложения, и как использовать Route Debugger для обнаружения возникших проблем во время выполнения. Но можно также писать модульные тесты для роутов, которые в первую очередь будут предотвращать возникновение таких проблем. В этом разделе мы рассмотрим, как тестировать обработку входных роутов, а также генерацию исходящих роутов.
Тестирование входящих роутов
По сравнению с остальной частью ASP.NET MVC Framework, тестирование роутов нельзя назвать легкой или интуитивной работой из-за количества абстрактных классов, для которых необходимо использовать mock-технологию. Чтобы сделать это вручную, потребуется много кода настроек, как показано в следующем листинге.
Листинг 9-11: Сложный способ тестирования роутов
using System.Web;
using System.Web.Routing;
using NUnit.Framework;
using Rhino.Mocks;
namespace RoutingSample.Tests
{
[TestFixture]
public class NotUsingTestHelper
{
[Test]
public void root_matches_home_controller_index_action()
{
const string url = "~/";
var request = MockRepository.GenerateStub<HttpRequestBase>();
request.Stub(x => x.AppRelativeCurrentExecutionFilePath).Return(url).Repeat.Any();
request.Stub(x => x.PathInfo).Return(string.Empty).Repeat.Any();
var context = MockRepository.GenerateStub<HttpContextBase>();
context.Stub(x => x.Request).Return(request).Repeat.Any();
RouteTable.Routes.Clear();
MvcApplication.RegisterRoutes(RouteTable.Routes);
var routeData = RouteTable.Routes.GetRouteData(context);
Assert.That(routeData.Values["controller"],
Is.EqualTo("Home"));
Assert.That(routeData.Values["action"],
Is.EqualTo("Index"));
}
}
}
Строка 15-20: Настройка mock-запроса
Строка 22-23: Регистрация роутов
Если бы все тесты роутов выглядели так, как показано в листинге 9-11, никто бы даже и не потрудился их тестировать. Специфические заглушки на HttpContextBase
и HttpRequestBase
тоже не стали удачным решением. Они заглядывали внутрь инструмента Red Gate’s Reflector, чтобы выяснить, для чего использовать mock. Это неправильное поведение для тестируемой платформы!
К счастью, проект MvcContrib включает в себя удобный и быстрый API для тестирования роутов, который значительно упрощает работу. Для начала мы должны установить сборку MvcContrib.TestHelper с помощью команды Install-Package MvcContrib.Mvc3.TestHelper-ci
в NuGet Package Manager Console, как показано на рисунке 9-9.
Рисунок 9-9: Установка MvcContrib Test Helper через NuGet

Следующий листинг представляет собой тот же тест, но с использованием расширений MvcContrib для тестирования роутов.
Листинг 9-12: Чистое тестирование роутов при помощи проекта TestHelper
MvcContrib
[TestFixtureSetUp]
public void FixtureSetup()
{
RouteTable.Routes.Clear();
MvcApplication.RegisterRoutes(RouteTable.Routes);
}
[Test]
public void root_maps_to_home_index()
{
"~/".ShouldMapTo<HomeController>(x => x.Index());
}
Строка 5: Регистрация роутов приложения
Строка 10: Утвердить URL для соответствия с действием
Начнем с регистрации роутов нашего приложения в TestFixtureSetUp
с помощью статического метода RegisterRoutes
из Global.asax
. Фактически сам тест выполняется методами расширения и лямбда-выражениями. Внутри тестового вспомогательного метода MvcContrib есть метод расширения для класса string
, который создает экземпляры RouteData
на основе параметров URL. Класс RouteData
имеет метод расширения для подтверждения того, что значения роута соответствуют контроллеру и действию.
В листинге 9-12 можно увидеть, что имя контроллера выводится из аргумента универсального типа в вызове метода ShouldMapTo<TController>()
. Действие тогда определяется с помощью лямбда-выражения. Выражение разбивается так, чтобы получить вызов метода (действия) и любые аргументы, которые будут ему переданы. Аргументы сопоставляются со значениями роута. Более подробную информацию об этих расширениях для тестирования роутов можно найти на сайте MvcContrib http://mvccontrib.org.
Теперь пришло время применить их к правилам маршрутизации нашего магазина и убедиться, что мы охватили все необходимые случаи.
Листинг 9-13: Тестирование роутов из нашего примера
using System.Web.Routing;
using MvcContrib.TestHelper;
using NUnit.Framework;
using RoutingSample.Controllers;
namespace RoutingSample.Tests
{
[TestFixture]
public class UsingTestHelper
{
[TestFixtureSetUp]
public void FixtureSetup()
{
RouteTable.Routes.Clear();
MvcApplication.RegisterRoutes(RouteTable.Routes);
}
[Test]
public void root_maps_to_home_index()
{
"~/".ShouldMapTo<HomeController>(x => x.Index());
}
[Test]
public void privacy_should_map_to_home_privacy()
{
"~/privacy".ShouldMapTo<HomeController>(x => x.Privacy());
}
[Test]
public void products_should_map_to_catalog_index()
{
"~/products".ShouldMapTo<CatalogController>(x => x.Index());
}
[Test]
public void product_code_url()
{
"~/products/product-1".ShouldMapTo<CatalogController>(x => x.Show("product-1"));
}
[Test]
public void product_buy_url()
{
"~/products/product-1/buy".ShouldMapTo<CatalogController>(x => x.Buy("product-1"));
}
[Test]
public void basket_should_map_to_catalog_basket()
{
"~/basket".ShouldMapTo<CatalogController>(x => x.Basket());
}
[Test]
public void checkout_should_map_to_catalog_checkout()
{
"~/checkout".ShouldMapTo<CatalogController>(x => x.CheckOut());
}
[Test]
public void _404_should_map_to_error_notfound()
{
"~/404".ShouldMapTo<ErrorController>(x => x.NotFound());
}
[Test]
public void ProductsByCategory_MapsToWebFormPage()
{
"~/Products/ByCategory".ShouldMapToPage("~/ProductsByCategory.aspx");
}
}
}
Каждый из этих простых тестов использует интерфейс тестирования NUnit. Они также используют метод расширения ShouldMapTo<T>
из MvcContrib.TestHelper
.
Примечание
Заключительный тест использует другой метод из MvcContrib -
TestHelper
. МетодShouldMapToPage
гарантирует, что URL соответствует конкретной странице Web Forms. Это бы вызвало ошибку маршрутизации, которую мы рассмотрели ранее. Если у вас есть юнит тесты для роутов, вы потратите меньше времени на их отладку.
Запустив этот пример, мы увидим, что все наши роуты работают нормально. На рис 9-10 показаны результаты в ReSharper test runner (могут выглядеть несколько по-другому, в зависимости от вашей тестируемой платформы и версии программы).
Рисунок 9-10: Результаты тестирования роутов в ReSharper test runner

Примечание
В листинге 9-13 мы создали для каждого правила отдельный тест. Может быть заманчиво объединить все эти одностроковые тесты в один, но не забывайте, как важно понимать то, почему тест не работает. Если вы допустили ошибку, только отдельные тесты перестанут работать, и вам будет легче понять причину, нежели когда перестанет работать один тест типа
test_all_routes()
.
Вооружившись этими тестами, вы можете свободно изменять правила роутов, не беспокоясь о том, что после этого у вас на сайте перестанут работать ссылки. Представьте себе, что ссылки на товары на Amazon.com неожиданно перестали бы работать из-за опечатки в каком-нибудь правиле роута... Не позволяйте такому случиться с вашим сайтом. Гораздо легче писать автоматизированные тесты для сайта, чем вручную проводить исследовательское тестирование для каждого релиза.
Тестирование исходящих роутов
В тестировании роутов есть один важный аспект, которому мы до сих пор уделяли мало внимания: исходящая маршрутизация. Как было определено ранее, исходящая маршрутизация относится к URL, которые генерируются платформой по данному набору значений роута. Вспомогательные методы для тестирования генерации исходящих роутов также включены в проект MvcContrib и показаны в следующем листинге.
Листинг 9-14 : Тестирование генерации исходящих URL
[TestFixtureSetUp]
public void FixtureSetup()
{
RouteTable.Routes.Clear();
MvcApplication.RegisterRoutes(RouteTable.Routes);
}
[Test]
public void Generates_products_url()
{
OutBoundUrl.Of<CatalogController>(x => x.Show("my-product-code"))
.ShouldMapToUrl("/products/my-product-code");
}
В этом примере мы тестируем роут для страницы товара нашего приложения. С помощью метода OutBoundUrl.Of
мы можем убедиться, что при передаче в движок маршрутизации имени контроллера catalog
, действия show
и товарного кода my-product-code
он будет генерировать URL /products/my-product-code
.
Теперь, когда мы рассмотрели исчерпывающий пример схем маршрутизации, мы можем начинать создавать роуты для своих собственных приложений и получать удобные, доступные URL. Мы также познакомились с некоторыми полезными расширениями для модульного тестирования, которые помогают гораздо проще тестировать входящие роуты.