23 февраля 2016 г.

Приветствую! Этим постом я открываю серию статей по тестированию различных компонентов приложений. Логика программы, пользовательский интерфейс, база данных - все это входящие в состав практически любой программы компоненты, подход к созданию которых диаметрально отличается друг от друга. Что и говорить - эти компоненты, или как их чаще называют - слои приложения, даже могут создаваться разными группами разработчиков. Совершенно естественно, что подход к тестированию таких непохожих частей программы не может быть абсолютно унифицирован.

Предлагаю начать обсуждение с относительно простой для тестирования системы маршрутизации 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, упрощает и унифицирует их написание.