Как правило, под тестированием AJAX приложений понимают обширный список тестов интерфейса и, в идеале, какое-то покрытие юнит-тестами. В этой статье мы перечислим основные проблемы такого подхода и, на примере простого приложения, использующего GWT, продемонстрируем эффективную стратегию тестирования, которая выходит за пределы тестирования интерфейса.
Недостатки тестирования интерфейса В целом, тестирование интерфейса: - ресурсоемкое – требует много времени на написание и выполнение тестов
- дает ограниченное представление о работе системы
- часто ограничивается только «правильными маршрутами» (happy paths)
- проверяет разные факторы в одном тесте
- зависит от внешних факторов
- требует дополнительной работы по настройке системы
- возникающие проблемы сложно отслеживать и воспроизводить
Хотя юнит-тесты не страдают от большинства из этих проблем, их нельзя оставить как единственный способ тестирования, поскольку они: - дают ограниченное представление о взаимодействии компонентов друг с другом
- не дают уверенности в том, что бизнес-логика и функциональность системы соответствуют требованиям
Хотя данная проблема и не имеет решения, которое удовлетворило бы всех, есть ряд принципов, которые делают тестирование веб-приложений более эффективным: - разработка совместных тестов для модулей (требуется определить наименьшую подсистему)
- распределение задач (подготовка к тесту не должна производиться через тестируемый интерфейс)
- независимое тестирование интерфейсов (с заменой «заглушками» всего, кроме тестируемых модулей)
- определение и использование зависимостей между модулями
- использование разных стратегий и инструментов
- нет, отказаться от тестирования интерфейса не получится
Подход к тестированию Руководствуясь вышеперечисленными принципами, мы предлагаем следующий подход к тестированию веб-приложений: - Изучите функциональность системы.
- Определите архитектуру системы.
- Определите способы взаимодействия компонентов системы.
- Определите взаимосвязи и условия, приводящие к ошибкам.
- Для каждой функции:
- Определите используемые компоненты.
- Определите потенциальные проблемы.
- Создайте независимые тесты для каждой из проблем.
- Создайте тест для «правильного маршрута».
Примечание: ценность теста Разработчики часто спрашивают при написании тестов: «а стоит ли тратить на это время?» Краткий ответ на этот вопрос: «всегда!» Поскольку исправление дефекта намного дороже, чем его предотвращение, создание хороших тестов всегда стоит потраченного на это времени. Существует множество способов классифицировать тесты, наиболее распространенный из них основан на размере и области применения теста – каждый тест отвечает на свои вопросы о качестве продукта: - Юнит-тест: выполняет ли функция свое назначение?
- Простой тест совместимости: могут ли два класса взаимодействовать друг с другом?
- Тест совместимости: работает ли класс корректно со всеми связанными классами? Корректно ли он обрабатывает ошибки? Все ли необходимые функции могут быть вызваны через графический интерфейс?
- Тест подсистемы: корректно ли две подсистемы взаимодействуют друг с другом? Способна ли каждая из них обрабатывать ситуация, когда другая подсистема работает неправильно?
- Тест системы: работает ли корректно вся система в целом?
Помните, что полезными являются те тесты, которые предоставляют быстрые и полезные результаты, то есть быстро определяют проблему и четко идентифицируют место ее появления. Перейдем к применению предложенного подхода на практике. В качестве примера мы рассмотрим простую систему управления ресурсами, позволяющую изменять количество имеющихся ресурсов в различных офисах. Это приложение использует GWT (Google Web Toolkit), но предлагаемый подход может быть применен к любому AJAX-приложению. Пройдем последовательно по всем шагам. 1. Изучите функциональность системы. Как бы просто это ни звучало, это ключевой шаг в тестировании приложения. Вам нужно понять, как функционирует система – с точки зрения пользователя – прежде чем вы сможете начать писать тесты. Откройте приложение, осмотритесь, понажимайте на кнопки и ссылки, и просто «почувствуйте» приложение. Вот как выглядит приложение в нашем примере: Приложение имеет навигационную панель, позволяющую отображать ресурсы по названию офиса, просматривать список ресурсов в каждом офисе, уменьшать/увеличивать количество ресурсов, и сортировать список по офису и по ресурсу. 2. Определите архитектуру системы. Изучение архитектуры системы – это следующий важный шаг. На этом этапе думайте о системе как о множестве компонентов, и представьте себе, как они общаются друг с другом. Изучение документации и архитектурных диаграмм полезно на этом шаге. В нашем примере мы имеем следующие компоненты: - GWT-клиент: код Java, скомпилированный в JavaScript, исполняемый браузером пользователя; связывается с сервером через RPC по HTTP
- Сервлет: стандартный сервлет Apache Tomcat, который обслуживает «frontend.html» (главную страницу) с внедрённым JavaScript, а также обслуживает вызовы RPC для связи с клиентской частью JavaScript
- Серверный компонент RPC: взаимодействует с сервлетом (с помощью вызовов RPC через HTTP) и с backend-компонентом (путем передачи буферов протокола (protocol buffers) через RPC)
- Backend-компонент: занимается бизнес-логикой и работой с данными
- База данных: отвечает за хранение данных, использует модель Bigtable
Это поможет нарисовать простую диаграмму, представляющую потоки данных между этими компонентами (если такой схемы все ещё не существует): 3. Определите интерфейсы между компонентами. Некоторые очевидные интерфейсы: - Модуль gwt_module в файле сборки Ant
- Сервлет Apache Tomcat
- Реализация интерфейса RPC
- Буферы протокола
- База данных
- Пользовательский интерфейс
4. Определите взаимосвязи и условия, приводящие к ошибкам. Теперь нужно определить зависимости между интерфесками, и значения, которые можно использовать для проверки обработки ошибочных ситуаций. В нашем случае, пользовательский интерфейс обращается к сервлету, который обращается к StoreService (серверный компонент). Нам нужно проверить,что происходит, если StoreService: - возвращает NULL
- возвращает пустой список
- возвращает очень большой список
- возвращает список с некорректным содержимым (неправильная кодировка, NULL-ы или пустые строки)
- получает два параллельных запроса
Кроме того, StoreService (серверный компонент) обращается к OfficeAdministration (backend-компонент). Опять нам нужно проверить обработку ошибочных ситуаций – что происходит, когда backend-компонент: - возвращает некорректные данные
- не отвечает в течение ожидаемого времени
- посылает два параллельных запроса
- вызывает обработчик исключений
Чтобы выполнить эти проверки, мы заменим StoreService модулем-«заглушкой», поведение которого мы можем контролировать, и проанализируем взаимодействие сервлета и заглушки. Аналогично, мы заменим OfficeAdministration на более простой модуль-«заглушку», и заставим StoreService взаимодействовать с ним. Для получения более четкого представления о взаимодействии компонентов, рассмотрим отдельные пользовательские сценарии. В качестве примера возьмем функцию фильтрации в пользовательском интерфейсе (в списке должны отображаться только ресурсы, относящиеся к офисам, помеченным галочкой). Анализ фильтра навигационной панели Клиент (пользовательский интерфейс): - получает список офисов через RPC
- при помечании офиса, запрашивает список его ресурсов через RPC, и обновляет таблицу при получении списка
- при снятии пометки с офиса, убирает его ресурсы из таблицы
Серверный компонент: - получает список офисов от backend-компонента
- получает список ресурсов для каждого офиса от backend-компонента
Backend-компонент: - делает выборку офисов из базы данных
- делает выборку ресурсов для заданного офиса из базы данных
Наш следующий шаг – определить «минимальный тест», который позволит нам проверить, что все компоненты работают правильно. Проверка функциональности клиента Давайте проверим, что снятие пометки с офиса прячет его список ресурсов. Для этого нам небходимо знать, какие строки есть в таблице. Серверный компонент-«заглушка» может ограничиваться только этой задачей – и выдавать отдельный список, независимый от других тестов, работающих с теми же данными. Наша задача – сделать так, чтобы сервлет использовал MockStoreService – наш компонент-заглушку – в качестве серверного компонента. Этого можно добиться несколькими способами: - ввести флаг для переключения между компонентами
- использовать шаблоны
- переключать компоненты во время выполнения
- добавить дополнительный конструктор в сервлет
- создать отдельные настройки сборки приложения, которые используют компонент-заглушку
- использовать внедрение зависимости (dependency injection) для замены полнофункционального компонента заглушкой
Каждый из этих способов будет работать, выбор зависит от приложения. Решение добавить новый конструктор в сервлет приведет к зависимости рабочего кода от тестового, что, очевидно, не есть хорошо. Переключение между компонентами во время выполнения (путем манипуляций с загрузкой классов) будет работать, но может привести к дополнительным уязвимостям в системе защиты. Внедрение зависимости является гибким и удобным способом получить желаемый результат без вмешательства в рабочий код. Для реализации внедрения зависимости существуют различные модули; мы хотели бы предложить один из них, GuiceBerry – это модуль, который позволяет использовать структурную модель для компонентов приложения. Другими словами, если ваш тест зависит от определенных компонентов, вы можете «внедрить» их в приложение с помощью популярного инструмента Guice. В нашем примере, нам необходимо пометить объект StoreService ключевым словом «@Inject» в описании класса сервлета, и создать дополнительную имплементацию – MockStoreService – чтобы внедрить ее во время выполнения. Это можно сделать следующим образом: @GuiceBerryEnv(StoreGuiceBerryEnvs.NORMAL) public class StorePortalTest extends TestCase { @Inject StoreServiceImpl storeService; public void testStorePortal() { ... storeService.doSomething(); ... } } Обратите внимание на строки, выделенные курсивом – это дополнения для GuiceBerry, позволяющие нам внедрить объект StoreServiceImpl в класс StorePortalTest. Определение StoreServiceImpl происходит в специальном классе GuiceBerry, называющемся NormalStoreGuiceBerryEnvs (и связаном с StorePortal через класс StoreGuiceBerryEnvs). Чтобы внедрить серверный компонент-«заглушку» в StorePortalTest, нам нужно создать MockStoreGuiceBerryEnvs (который породит «заглушку» для StoreService) и подменить им NormalStoreGuiceBerryEnvs во время выполнения. Все, что нам останется сделать – это указать следующие параметры JVM для теста: JVM_ARGS="-DNormalStoreGuiceBerryEnvs=MockStoreGuiceBerryEnvs" Этот пример демонстрирует только небольшую часть возможностей GuiceBerry; подробнее о нем можно узнать на официальном сайте. Этих изменений будет достаточно, чтобы отделить компонент-клиент от остальных –всю остальную работу сделает GwtTestCase; подробнее об этом можно почитать в этой статье. Не забудьте внедрить обработку остальных ошибочных сценариев через MockStoreService. Продолжение следует… (по материалам Google Testing Blog) |