Разработка на основе тестирования: "сверху вниз" или "снизу вверх"?
Разработка на основе тестирования — это процесс программирования, в котором модульные тесты используются для определения поведения системы перед написанием фактической реализации. Разработка на основе тестирования для JavaScript была рассмотрена в ScriptJunkie ранее автором Элайджа Манор (Elijah Manor). В данной статье я хочу обсудить различие между разработкой снизу вверх и сверху вниз. Разработка "снизу вверх" Разработка "снизу вверх" — это традиционно наиболее распространенный способ осуществления разработки на основе тестирования. Выбор подхода "снизу вверх" соответствует началу с независимых низкоуровневых объектов и функций, таких как настраиваемый интерфейс AJAX, построенный на базе такой библиотеки, как jQuery, которая специально разработана для внутренних ресурсов вашего приложения, например, Разработка "сверху вниз" При разработке "сверху вниз" (или снаружи внутрь) вы обычно начинаете с пользовательского интерфейса и пишете тесты, которые включают в себя взаимодействие с пользователем. После этого вы работаете по этим тестам, "погружаясь все глубже", пока не получите полностью функциональный компонент. Обычно широко используются заглушки или макеты, поскольку базовые интерфейсы еще не существуют. Эти заполнители часто служат в качестве начальной точки для тестов на следующем уровне. Разработка "сверху вниз", а затем "снизу вверх" Подход "сверху вниз" к разработке ценится его сторонниками за то, что он предоставляет разработчикам возможность сильнее сконцентрировать внимание на ценности для бизнеса, а также имеет пониженный риск того, что написанный код будет отброшен за ненадобностью. С другой стороны, преимущество подхода "снизу вверх" заключается в слабо связанных элементах повторно используемого кода. Разработка "снизу вверх" также требует значительно меньше заглушек или макетов. Лично я всегда предпочитал разработку "снизу вверх". Мне нравится повторно используемый код. Очень нравится. Мне представляется разумным определять отдельные функциональные возможности, независимо реализовывать их и связывать их друг с другом для предоставления конечному пользователю. Однако сложно спорить с практикой, которая обещает повысить мою сосредоточенность на ценности создаваемого кода для бизнеса. В данной статье я покажу, как можно немного сдобрить процесс разработки подходом "сверху вниз", чтобы четко следовать приоритетам, разделяя повторно используемые компоненты в соответствии с подходом "снизу вверх". Оговорка: я не говорю о том, что вы не можете получить слабо связанные элементы при разработке "сверху вниз". Я говорю о том, что подход "сверху вниз" позволяет получить гораздо более естественный результат. Поставленная задача: поиск в режиме реального времени Чтобы проиллюстрировать такой смешанный подход, я покажу вам примеры из разработки подключаемого модуля поиска в режиме реального времени для jQuery на основе тестирования. Для тех, кто не знаком с "поиском в режиме реального времени" (или "автозаполнением/автозавершением"), отметим, что это улучшение, которое можно применить, например, к формам поиска. По мере ввода пользователем под текстовым полем раскрывается список предложенных терминов, а элементы в этом списке можно выбирать для упрощения поиска требуемого элемента. Первый тест: высокоуровневый обзор Начиная с подхода "сверху вниз", мы напишем высокоуровневый функциональный тест, который описывает функциональные возможности с точки зрения пользователя. Чтобы избежать слишком большого числа макетов для реализации, этот тест попытается как можно точнее описать, что делает наша система, и как можно меньше коснуться того, как это делается. Прежде всего, нам нужен тестовый случай и тест с описательным именем. Мы можем воспользоваться тем фактом, что JavaScript разрешает использовать строки в качестве имен свойств, и сделать именем небольшое предложение, описывающее предмет тестирования.
Добавление разметки Далее нам нужна форма для улучшения. Я запущу тесты с использованием JsTestDriver, который позволяет не использовать для запуска тестов файлы вспомогательного кода HTML, а создать их на сервере. В большинстве случаев отказ от вспомогательного кода HTML приносит пользу, поскольку сокращает объем организационных действий для написания и запуска тестов. Однако это значит, что нам нужно работать немного упорнее, чтобы внедрить образец разметки для использования тестами. Чтобы упростить создание образца разметки для тестов, JsTestDriver предоставляет компонент "HTMLDoc", который позволяет внедрить некоторую разметку в комментарий внутри теста. Этот комментарий анализируется на стороне сервера JsTestDriver, а итоговые узлы DOM назначаются свойству в тестовом случае (например, только в памяти, что работает быстрее) или прикрепляются к документу. Мы не хотим, чтобы подключаемый модуль поиска в режиме реального времени считал, что он является владельцем документа, поэтому помещение в память вполне подходит:
Инициализация модуля поиска в режиме реального времени После размещения формы нам необходимо инициализировать компонент. Это будет единственным случаем взаимодействия с нашим API в рамках этого высокоуровневого теста.
Имитация ввода пользователя Когда модуль настроен, нам необходимо ввести некоторые сведения и затем подтвердить, что выполняются правильные операции. Мы активируем только интересующие нас события ввода, в данном случае это событие
Эта часть довольно подробна. Мы может очистить это позднее, но перед прохождением теста; введение сложной логической схемы только повышает риск возникновения ошибок во время теста. Ход времени В приведенном выше фрагменте кода отсутствует одна вещь. Хотя Робокоп мог бы вводить текст со скоростью быстрее 1 миллисекунды, люди такого не могут, и это должно быть отражено в тесте. Кроме того, нам нужны задержки, чтобы убедиться, что запросы не будут отправлены до истечения минимального времени ожидания. Вместо того, чтобы заставлять тест ожидать после каждого нажатия клавиши, что было бы медленно, мы будем использовать библиотеку Sinon.JS для искусственной передачи времени:
Создание для тестового случая оболочки из вызова в Подтверждение отображения предложений Когда сервер отвечает, подключаемый модуль поиска в режиме реального времени должен отобразить предложения в виде узлов модели DOM в форме. Мы добавим несколько проверочных утверждений, которые проверяют ожидаемое число предложений, а также их порядок:
Это основа проверки теста. Однако у нас еще имеются не все сведения, поэтому пока невозможно узнать, сколько результатов следует ожидать или каково их содержимое. Для ответа на этот вопрос нам придется иметь дело с запросом к серверу. Тестирование взаимодействия с сервером По причинам, указанным в моей предыдущей статье ScriptJunkie, которая посвящена заглушкам и макетам, непосредственное подключение к рабочему серверу в рамках теста нежелательно. Это значит, что мы воспользуемся библиотекой Sinon.JS и при тестировании сервера. Запросы сервера-заполнителя обрабатываются библиотекой Sinon.JS в два прохода. Сначала мы определяем ответы, о которых должно быть известно серверу. Потом мы велим серверу отвечать на все запросы в удобное время. Мы можем определить ресурсы сервера в любое время, но поскольку это часть настройки теста, имеет смысл сгруппировать эту процедуру с остальным кодом настройки в начале теста:
Здесь мы сообщаем серверу, как обрабатывать запросы
Благодаря четкому обозначению данной информации в тесте заполнение проверочных утверждений осуществляется довольно легко:
Потребность во времени ожидания В нашем тесте все еще отсутствует одна деталь. Похоже, что мы ожидаем отсутствие запросов к серверу до того момента, когда пользователь прекратил ввод хотя на 150 миллисекунд. Однако проверочные утверждения, которые проверяют такое поведение, в нашем тесте отсутствуют. Чтобы выполнить данное требование, мы добавим еще одно проверочное утверждение сразу после последнего нажатия клавиши, но перед истечение финального временем ожидания, для подтверждения отсутствия запросов в течение указанного времени:
После этого тест завершен, и в нем легко просматриваются автоматические предложения:
Запуск теста Чтобы запустить тест, необходим небольшой файл конфигурации для JsTestDriver, сохраните его в jsTestDriver.conf:
Этот файл конфигурации просто загружает файл теста. Запустите сервер JsTestDriver, прикрепите браузер и запустите тест. Он ожидаемо завершается со сбоем. На данном этапе чтение сообщений об ошибках, отображаемых при запуске теста, позволит вам узнать о пошаговой настройке остальной части среды. После устранения всех ошибок у вас должен получиться файл конфигурации, который выглядит следующим образом:
Где каталог
На данном этапе тест приводит скорее к сбою, чем к ошибке, то есть тест выполняется правильно, но проверочные утверждения не проходят проверку. В этом нет ничего удивительного, так как мы еще не реализовали подключаемый модуль. Прохождение этого теста нельзя обеспечить без совершения гигантского прыжка в неизвестность, поэтому мы сместим акценты. Проход "снизу вверх": время ожидания Высокоуровневый функциональный тест теперь задает нам четкую цель, которая имеет значение для конечного пользователя. Вместо продолжения применения подхода "сверху вниз" с построения предположений о базовых интерфейсах с помощью макетов мы сейчас построим необходимые части подключаемого модуля снизу вверх. Время ожидания и организация очереди запросов являются существенным компонентом подключаемого модуля поиска в режиме реального времени и отличным местом для начала. Начиная с малого: организация очереди запросов в нужное время Как и всегда при проведении разработки на основе тестирования мы начнем с самого простого теста, который только можно себе представить. Этот тест будет ожидать, что сразу же после помещения запроса в очередь запрос не выполняется:
Повторим, что мы используем Sinon.JS для предоставления сервера-заполнителя, а тест проверяет наличие 0 запросом для сервера после вызова метода
Тест пройден. Затратив совсем немного усилий, мы добились прогресса, хоть и такого незначительного. Далее мы ожидаем попадания запроса на сервер после минимальной задержки:
Тест не требует от нас многого. Он ожидает отправки одного запроса — любого на сервер. Пройти его очень просто:
Хотя и видно, что данная реализация неполна, но она удовлетворяет условиям теста. Концентрация внимания на времени ожидания Предыдущий тест показал, что что-то назревает. Как и в случае с предыдущим функциональным тестом добавление значимых проверочных утверждений в тест является трудной задачей. В идеальном случае нам бы хотелось проверить, что URL-адрес запроса содержит в некоторой форме строку запроса. К сожалению, из теста неочевидно, как именно должен выглядеть URL-адрес, каким должно быть имя параметра запроса (при наличии) и даже какой именно запрос должен использоваться — GET или POST. Одна из проблем с предыдущим тестом заключается в том, что он не получает достаточно входных данных, чтобы мы смогли узнать, как и где будет выполнен запрос. Более значительная проблема заключается в том, что из-за такого недостатка информации мы отклоняемся от первоначальной цели: моделирования времени ожидания. Один возможный вывод показывает, что от
Теперь тест ожидает, что объект Чтобы обеспечить прохождение тестов реализацию следует также обновлять, итоговый результат выглядит следующим образом:
Несколько помещенных в очередь запросов Убрав с пути навязчивые запросы, мы можем продолжить работу со временем ожидания. Следующий тест проверит, что выполнение нескольких запросов продлевает минимальную задержку, требуемую для непосредственного запроса результатов:
Тест ждет в общей сложности 150 миллисекунд, но поскольку с момента прошлого запроса прошло всего 50 миллисекунд, мы ожидаем отсутствие выполненного запроса. Тест завершается со сбоем, так как запрос "Robocop" все еще передается в источник данных. Решение заключается в очистке всех имеющихся периодов времени ожидания при каждом помещении запроса в очередь:
Поскольку данный тест теперь выполняется, мы можем добавить еще один тест, который ожидает передачи самого последнего запроса в источник данных.
Данный тест выполняется сразу же, поэтому нам не нужно обновлять реализацию. Обратите внимание на то, что тест не проверяет двойной вызов источника данных, об отсутствии такого вызова нам известно из предыдущих тестов. Все, что мы хотели на данном этапе, это обеспечение передачи правильного запроса. Отклонение от благоприятного пути До сих пор мы тестировали только благоприятный путь для объекта поиска в режиме реального времени. Сейчас мы напишем тест, который ожидает отсутствие запроса данных для пустых запросов:
При текущей реализации
У меня такое чувство, что существенное значение имеет то, где мы прерываем метод
Мои подозрения подтвердились — этот тест завершается со сбоем. Чтобы исправить ситуацию, мы просто переносим проверку аргумента на время после отмены таймера:
Источник данных и отображение Экземпляр "Готовый" подключаемый модуль Приведенный ниже пример представляет собой "готовый" подключаемый модуль jQuery. Обратите внимание на кавычки — этот модуль готов только в том смысле, что он проходит наш первоначальный функциональный тест.
Если вам интересно, как я выбирал этапы при разработке на основе тестирования для двух оставшихся объектов, можете ознакомиться с полным проектом на GitHub. Функциональный тест после рефакторинга Ранее я упомянул, что не хочу уменьшать уровень детализации функционального теста, пока он не будет пройден. Теперь мы можем это сделать. Один из способов уменьшения уровня детализации заключается в добавлении вспомогательной функции для ввода стройки в текстовом поле. Эта вспомогательная функция делит строку на символы и активирует событие нажатия клавиши отмечает время для каждого введенного символа. Итоговая версия функционального теста:
Заключение В данной статье мы рассмотрели процесс разработки на основе тестирования для подключаемого модуля jQuery. Мы начали работу с позиции пользователя, написав функциональный тест в стиле разработки "сверху вниз". После этого мы перешли к построению одного из необходимых объектов в стандартном стиле "снизу вверх". Данный процесс обеспечивает постоянное ориентирование на ценность для конечного пользователя и бизнеса с одновременным применением подхода "снизу вверх" для низкоуровневых частей нашей реализации. Данный процесс набирает популярность, а представленное здесь его описание приближается к понятию, которое часто называют разработкой на основе приемочного тестирования (Acceptance test-driven development — ATDD). Обратите внимание на то, что функциональный тест, с которого мы начали, необязательно является тестом приемки, так как он проверяет только части компонента (то есть отсутствует навигация с помощью клавиатуры, отсутствуют обработчики щелчков для предложений и так далее). Но все равно он иллюстрирует, как учет функциональных возможностей для пользователя помогает нам выбирать правильные объекты для реализации, даже когда мы добираемся до самых основ. |