Тестирование маршрутов 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,
упрощает и унифицирует их написание.