Тестирование маршрутов ASP.NET MVC приложения
Приветствую! Этим постом я открываю серию статей по тестированию различных компонентов приложений.
Логика программы, пользовательский интерфейс, база данных - все это входящие в состав практически любой программы компоненты,
подход к созданию которых диаметрально отличается друг от друга. Что и говорить - эти компоненты, или как их чаще называют - слои приложения,
даже могут создаваться разными группами разработчиков. Совершенно естественно, что подход к тестированию таких непохожих частей программы не может быть 
абсолютно унифицирован.
Предлагаю начать обсуждение с относительно простой для тестирования системы маршрутизации ASP.NET MVC приложения.
Даже если в вашем проекте не применяются технологии модульного тестирования, тесты маршрутов могут служить неплохим стартом для написания тестов, 
так же как сами маршруты представляют начало обработки запроса в конвейере MVC приложения.
К тому же для маршрутов невероятно важно понятие отсутсвия регрессии - неверное изменение или удаление маршрутов уже существующей схемы маршрутизации 
в работающем приложении, или ошибки в схеме могут значительно понизить посещаемость и уважение сайта из-за битых ссылок, ведущих в никуда. 
Вдобавок, в процессе развития приложения, маршруты стремятся переплестись в сложнопонимаемые сети, приводящие к параличу изменений 
(любые, даже самые незначительные, изменения сопровождаются огромным стрессом из-за страха что-либо сломать), 
и именно наличие тестов на систему маршрутизации способно 
облегчить ее понимание и развитие.
Как вы убедитесь, в итоге получаются очень простые и понятные тесты, визуально состоящие только из assert секции. Но как же этого добиться?
Обзор возможности для тестирования
Для тестирования маршрутов требуется не столь многое: мок HttpContextBase, мок HttpRequestBase, и, конечно, не стоит забывать о моке
HttpResponseBase для тестирования исходящих Uri. Необходимые перегрузки членов для этих контекстов можно найти в 
github репозитории проекта,
которому посвящена эта статья. Создание моков с одной стороны позволяет глубже понять инфраструктуру MVC фреймворка,  но все это не укладывается в 
обещанную простоту тестов, к тому же такое решение не подчиняется принципу DRY, 
из-за постоянного повторения одного и того же кода для создания моков в ваших проектах.
Конечно, повторяемость одного и того же кода вынудило разработчиков создать общее решение для инкапсуляции сложности предварительной подготовки http 
контекстов для тестирования. Я говорю о небольшой библиотеке MvcRouteTester, 
которая предначенна 
специально для тестирования системы маршрутизации с небольшой дозой синтаксического сахара. Теперь не нужно создавать моки, библиотека сделает их за нас, 
поэтому можно убрать в сторону и забыть все, что отвлекает от непосредственно тестов, и, наконец, приступить к их написанию.
Предварительная инициализация
Практически каждая библиотека, или, тем более фреймворк, требует некоторой начальной настройки, которые мы называем инициализацией.
Большинство тестов, в т.ч. тесты маршрутов, имеют предварительную Arrange секцию. К счастью, для тестов маршрутов Arrange секция не только 
одинакова для всех тестов, но и занимает одну-единственную строку: инициализация коллекции маршрутов.
public class RoutesTests {
 private readonly RouteCollection routes = new RouteCollection();
 public RoutesConfigTests()
 {
  new RoutesConfig().Configure(routes);
 }
}
В зависимости от используемого фреймворка тестирования метод инициализации размещается в разных местах кода:
Для xUnit, который использую я, таким местом инициализации служит конструктор, для 
NUnit, придется выделить метод Initialize
и пометить его аттрибутом SetUpAttribute, или TestInitializeAttrribute для MsTest.
Займемся тестами
Библиотека предлагает два практически равнозначных способа описания маршрутов:
через статичесткие методы класса RouteAssert или использование так называемого fluent синтаксиса.
Предлагаю начать написание тестов маршрутов через RouteAssert. Визуально описание маршрутов очень похоже на одну assert секцию, поэтому
разобраться в простых примерах не должно составить особого труда:
public void RoutesTest()
{
 // Главная страница сайта
 RouteAssert.HasRoute(routes, "/",
  "Articles", "Show", new { page = 1 });
  
 // Страница статьи блога
 // Конечно, не нужно описывать их все, достаточно одного-двух тестов
 RouteAssert.HasRoute(routes, "/test-mvc-routes", 
  "Article", "Show", new { name = "test-mvc-routes" });
 ...
 
 // Несуществующие маршруты тоже не помешало бы проверить
 RouteAssert.NoRoute(routes, "/test-mvc-routes/more-segment");
}
Обратите внимание, на предварительный слеш и отсутствие имени домена, (ведь маршруты абсолютно независимы от конкретного доменного имени).
Посмотрите, как легко читаются тесты:
например, первое утверждение 
RouteAssert.HasRoute(routes, "/", "Articles", "Show", new { page = 1 }); проверяет, 
что, для коллекции маршрутов routes (напомню, коллекция тестируемых маршрутов задается при инициализации тестов), запрос пользователем страницы 
"http://your-domain-name/" будет обработан методом действия "Show" контроллера "ArticlesController", 
вдобавок, методу действия будет передан параметр page со значением 1.
Как и в большинстве мест MVC фреймворка указание постфикса "Controller" для контроллеров не требуется.
Гуру тестирования неустанно повторяют правило "Один тест - один ассерт" (не один вызов Assert метода в конце теста, 
а одна логическая секция Assert, которая без сомнения может содержать несколько Assert утверждений).
Но думаю, что в данном случае выделение отдельного метода размером в одну строку не имеет особого практического смысла и лучше группировать 
несколько тестов маршрутов по обрабатывающему их контроллеру.
Описание тестов во fluent стиле
Используя fluent подход для написания тестов, можно получить, возможно, с какой-то точки зрения, более читабельные тесты.
Для этой цели fluent подход использует мощь деревьев выражений для указания цели маршрута.
Посмотрите на те же самые тесты, записанные во fluent синтаксисе:
public void RoutesTest()
{
 // Главная страница сайта
 routes.ShouldMap("/")
  .To<ArticlesController>(a => a.Show(1));
 routes.ShouldMap("/test-mvc-routes")
  .To<ArticleController>(a => a.Show("test-mvc-routes"));
 routes.ShouldMap("/test-mvc-routes/more-segment")
  .ToNoRoutes();
}
Здесь выражения вида 
a => a.Show(1) - не просто анонимный метод (лямбда выражение),
а так называемое дерево выражений, которое, грубо говоря, "парсится" библиотекой, в итоге получая те же самые 
"Article", "Show", new { page = 1 }, как и при использовании RouteAssert подхода, но, согласитесь, гораздо читабельнее.
Какой стиль использовать - дело личных предпочтений, однако в пределах одного проекта я настоятельно рекомендую следовать единому стилю.
Вообще, следование единому стилю, пусть, возможно, не самому верному, для будущей поддержки более предпочтителен зоопарку стилей и подходов.
В итоге, несмотря на возможно кажущуюся сложность предварительной настройки контекстов для тестирования, которая в конечном итоге
скрывается за утилитными методами, или внутри библиотеки, тесты маршрутов получаются чрезвычайно просты и понятны. 
Они легко смогут определить и указать на регрессию системы маршрутизации.
Благодаря этому, система маршрутизации представляется неплохим стартом для внедрения модульного тестирования в процесс разработки, а использование 
библиотеки MvcRouteTester, конечно, доступную через nuget, 
упрощает и унифицирует их написание.