Selenium. Интеграционное тестирование веб-приложений

8 декабря 2010 г.

Веб-приложения сейчас очень популярны. Почти ежедневно в сети появляется пост об очередном интересном стартапе. Однако, очень часто в обсуждении всплывают неприятные комментарии от пользователей о том, что не работает заявленная функциональность, или в некоторых браузерах приложение ведёт себя странно.

Я занимаюсь интеграционным тестированием веб-приложений около года, и могу сказать, что интеграционные тесты отлично помогают преодолеть подобные проблемы. К сожалению, нигде нет подробного руководства по тому, как писать интеграционные тесты, как организовывать процесс и главное — какие преимущества они дают.

Если вы хотите узнать о том, что такое автоматизированные интеграционные тесты, как они помогают улучшить качество продукта и избежать проблем — прошу под кат.

Если вы считаете, что TDD и юнит-тестов достаточно, или что ручное тестирование приложения предпочтительнее — я изложил свои мысли по этому поводу, они находятся в последней части статьи.

Очень многие разработчики веб-приложений рано или поздно сталкиваются со следующими проблемами:

  • Внезапно вылез баг в приложении на живом сервере, срочно исправлять!
  • Я внёс крупные изменения в код. Где гарантия, что реализованная функциональность не пострадает?
  • Я перешёл на новый фреймворк или библиотеку. Где гарантии, что не будет проблем с совместимостью?

Я наберусь наглости и заявлю, что автоматизированные интеграционные тесты с лёгкостью помогут справиться с этими проблемами.

Интеграционные тесты тестируют систему по принципу «чёрного ящика». Они абсолютно ничего не знают о том, КАК реализован тестируемый функционал. Единственные средства воздействия на приложение — имитация действий пользователя в UI и изменение данных, хранящихся в БД.

Важно отметить, что при внедрении интеграционных тестов код приложения никак не изменяется. В коде нет компонентов-заглушек, которые при заливке на живой сервер подменяются реальными компонентами. В коде нет конструкций «if (testing) {…} else {…}».

Типичный сценарий интеграционного теста выглядит так:

  1. В базу данных при необходимости помещаются данные, необходимые для теста
  2. С помощью специализированных инструментов (о них — ниже) в тесте имитируются действия пользователя.
  3. Проверяется состояние UI — пользователю должно отображаться именно то, что нужно.
  4. Проверяется состояние БД — все данные в БД должны соответствовать ожидаемым.
  5. Перехватываются исходящие сообщения от приложения — например, исходящие e-mail.

Теперь я расскажу о каждом этапе сценария подробнее.

Взаимодействие с UI.

Для имитации действий пользователя и воздействия на UI мы используем Selenium. Мы выбрали его в самом начале проекта, и его возможности устроили нас полностью. Ни разу не возникало необходимости смены инструмента из-за того, что не хватало функциональности или возникали какие-то странные ошибки.

Уже были посты о Selenium, здесь можно посмотреть видео того, как работает Selenium, а тут — схему его работы.

Начать писать интеграционные тесты с использованием Selenium очень просто. (Ниже — пример кода).
Нужно просто запустить SeleniumServer, который принимает входящие сообщения и транслирует их браузеру.
При конфигурировании Selenium Server нужно указать всего 4 параметра: адрес и порт selenium сервера, используемый браузер и адрес, по которому расположено тестируемое веб-приложение.

public class SeleniumExampleTests
{
[Fact]
public void SeleniumExample1()
{
ISelenium selenium = new DefaultSelenium("localhost",
5561,
"*firefox",
"localhost:8099");

selenium.Start();

selenium.Open("/Account/LogOn");
selenium.WaitForPageToLoad("10000");

selenium.Type("Name","username");
selenium.Type("Password","12345");

selenium.Click("//input[@value='Войти']");
selenium.WaitForPageToLoad("10000");

Assert.True(selenium.IsTextPresent("Вход успешен!"));

selenium.Stop();
}
}

Как видим, код достаточно прост и не требует особых комментариев.

Главные преимущества Selenium — это кроссбраузерность и поддержка многих языков. Можно изменить версию используемого браузера без перезапуска Selenium Server. Мне доводилось встречать много негативных отзывов на форумах о поддержке браузеров в Selenium, но подобные посты написаны давно, и с тех пор многое изменилось.

Мы тестировали наше приложение в IE, Firefox, Google Chrome и никаких проблем из-за неполной поддержки браузера не возникло. Также поддерживается Opera и Safari.

В видео по ссылке выше было показано, что selenium поддерживает C#, Java, PHP, Ruby и Perl. На Самом Деле поддержка этих языков означает, что для них уже написаны и протестированы готовые обёртки. Никто не мешает использовать selenium из любого другого языка программирования, главное, чтобы в этом языке была возможность сформировать http-запрос и отправить его selenium server. Подробнее об этом — здесь.

Однако, написание интеграционных тестов только с использованием Selenium, на мой взгляд, является неполным. Для полноты тестов необходимо проверять данные, которые сохраняются в БД, например, логи, состояние объектов и пр.

Взаимодействие с БД

В нашем проекте мы используем Fluent Nhibernate. Он очень гибок, не требует написания xml-конфигураций (а следовательно, не возникает проблем с маппингами при рефакторинге) и имеет очень обширную функциональность.

Преимущество интеграционных тестов в том, что их можно сделать очень гибкими. Можно писать их не только на любом языке программирования, но и использовать любую ORM (а можно обойтись и вовсе без ORM). Главное — поместить перед тестом необходимые данные в БД и проверить данные в БД после теста, а это можно сделать с помощью любого из доступных инструментов.

В наших проектах мы используем Domain-Driven Development, и интеграционные тесты «знают» только о доменных объектах. Этого вполне достаточно, чтобы поместить объекты в БД и проверить их состояние после теста. Таким образом, интеграционные тесты не собирают на себе много зависимостей о проекте, оставаясь гибкими.

Тестировщик, пишущий интеграционные тесты, работает с т.н. эталонной базой данных. Она полностью воссождаёт структуру реальной БД, которую использует реальный продукт. Отличие в том, что эталонная БД перед каждым тестом очищается, и для каждого теста в неё помещается тот минимум данных, который необходим именно для этого конкретного теста. Это делается для того, чтобы гарантировать, что никакие «мусорные» данные не повлияют на прохождение теста. Кроме того, поместить небольшой объём данных в БД можно очень быстро, и при необходимости запуск 2-3 интеграционных тестов не потребует длительного времени.

Перехват исходящих e-mail.

Мне доводилось видеть, что в некоторых проектах отправка e-mail либо не тестируется вообще, либо тестируются компоненты-заглушки, которые при выпуске новой версии проекта хитрым образом подменяются на реальные компоненты. Таким образом, реальные компоненты остаются непротестированными, их поведение может быть каким угодно. Главное — нарушается чистота тестирования и принцип «чёрного ящика», появляется вмешательство в тестируемое приложение, что, в итоге, осложняет жизнь всем.

В наших тестах мы используем фейковый smtp-сервер. Это обычный TcpListener, который слушает сообщения, поступающие на 25 порт на машине, на которой запускаются тесты. Его код достаточно тривиален, к тому же в Интернете в свободном доступе есть несколько аналогов. Я могу выложить этот код, если кому-то будет интересно.

Несколько слов о производительности

Вы можете сказать — «э, мы пробовали что-то подобное, но это оказалось очень уж медленно». При описанном выше сценарии работы тестов всё, как правило, проходит достаточно быстро. Наиболее ресурсоёмкой операцией является запуск браузера из Selenium, поэтому для всех тестов запускается только один экземпляр браузера. Команды от Selenium к браузеру поступают очень быстро, помещение или удаление нескольких объектов в БД также не становится «узким местом» в плане производительности. Конечно, по мере разрастания проекта увеличивается число тестов, и они неизбежно начинают выполняться всё дольше и дольше. Как мне кажется, мы нашли достаточно хорошее решение, но о нём — немного ниже, в части статьи, которая касается проблем автоматизированного интеграционного тестирования вообще.

Роль интеграционных тестов в проекте

Интеграционные тесты — не инструмент нахождения багов! Их роль намного шире, а информацию о найденных багах мы можем получать из разных источников, а не только от интеграционных тестов.

В наших проектах требуемую функциональность можно считать реализованной тогда и только тогда, когда на неё написан интеграционный тест и он проходит успешно. Написанный хороший код в стиле Test-Driven Development не гарантирует, что заявленный функционал будет работать. Например, некоторые объекты в тестах могут быть сконфигурированы неверно, или части системы не всегда корректно взаимодействуют между собой, хотя при этом все юнит-тесты проходят. Наконец, mock-объекты в юнит-тестах могут быть сконфигурированы неверно. Все подобные ошибки отлавливаются только интеграционными тестами.

Цикл разработки продукта в наших проектах выглядит так:

  • программист пишет unit-test
  • программист пишет код, реализующий требуемую функциональность
  • тестировщик пишет интеграционный тест
  • при необходимости программист вносит изменения в код
  • ???
  • PROFIT!

Важно, что программист не пишет интеграционный тест сам — этим занимается тестировщик. Это сделано, чтобы программист концентрировался только на написании нового кода, и как можно меньше отвлекался на исправление ошибок. Кроме этого, после того как программист написал код, у него в голове находится идеальный сценарий работы функциональности, которую он только что написал. Он может не подумать о каких-то хитрых случаях, которые способны вызвать баг. Наконец, интеграционные тесты проходят дольше, чем юнит-тесты, и если программист будет писать и запускать интеграционные тесты сам, это сломает его равномерный ритм работы, он будет отвлекаться и продуктивность неизбежно снизится. Test-Driven Development нужен для того, чтобы программист двигался небольшими шагами, но постоянно, сохраняя неизменный ритм работы. Если в стройную схему добавить интеграционные тесты, то ритм работы сохранить уже не удастся.

Главное преимущество, которое дают интеграционные тесты — лёгкость масштабных изменений в проекте. Переход на новую версию фреймворка или библиотеки проходит быстро и безболезненно. Если у нас есть набор тестов, полностью покрывающих функциональность проекта, то после смены фреймворка мы можем точно сказать, что для пользователя ничего не изменится и не сломается. Так, переход с ASP.NET MVC на ASP.NET MVC 2.0 в одном из наших проектов занял всего несколько часов — мы перешли на новую библиотеку, запустили все интеграционные тесты, исправили несколько появившихся ошибок и залили на сервер новую версию. В новой версии существующие сценарии работы пользователя гарантированно не изменились и в них не появилось ошибок. Переход с .NET 3.5 на .NET 4 занял полдня и проходил по такой же схеме.

Словосочетание «гарантированно не будет ошибок» повторяется не зря. После того, как ошибка один раз найдена, на неё написан интеграционный тест и ошибка исправлена, она гарантированно не появится снова и не будет раздражать программистов своим присутствием. С этим связано и ещё одно преимущество — автоматизированные тесты позволяют обнаружить ошибки так рано, как только возможно. Чем раньше обнаружена и исправлена ошибка, тем дешевле обходится её исправление. Более того, если программист только что закончил задачу, а на неё уже написан интеграционный тест и найдена ошибка, то программист исправит её очень быстро — вся задача ещё у него в голове, он не успел забыть детали реализации.

С экономической точки зрения автоматизированные тесты дают ещё одно преимущество — они дешевле ручных тестов. Держать штат тестировщиков, которые занимаются ручным тестированием, дороже, чем иметь несколько тестировщиков, которые пишут интеграционные тесты. К тому же, где гарантия, что тестировщик, который воспроизвёл нетривиальную последовательность действий и нашёл ошибку, вспомнит эту последовательность действий через неделю или через месяц?

Более того, как правило, тестировщики, которые пишут интеграционные тесты, знают архитектуру проекта почти так же хорошо, как и программисты. Практика коллективного владения кодом распространяется на них почти в той же степени, как и на программистов. Отдача от таких тестировщиков будет лучше, так как они знают архитектуру и потенциально лучше способны найти в ней ошибки, чем тестировщики, которые не знают языка программирования вообще.

Интеграционные тесты здорово «прикрывают спину» программисту — меньше ошибок вылезает в приложении на живом сервере, нет авралов, срочных исправлений после релиза и т.д. Программист спокоен и сосредоточен на написании нового кода высокого качества. Интеграционные тесты позволяют облегчить внедрение экстремального программирования в команде: сроки не будут срываться из-за того, что все бросились срочно исправлять критические ошибки, а рабочая неделя будет состоять из 40 часов, а не, скажем, 68.

Возможные проблемы

Естественно, в этой бочке мёда под названием «автоматизированные интеграционные тесты» есть и ложка дёгтя. Тестовое окружение должно максимально повторять окружение, настроенное на живом production-сервере. Если развёртывание БД или настройка сервера нетривиальны и требуют времени, то столкьо же времени придётся потратить на настройку сервера для тестов.

Интеграционные тесты, как и реальный код приложения, надо часто рефакторить. Но рефакторинг невозможен без полного прогона всех тестов. Если вы решили отрефакторить группу тестов, которые в сумме проходят в течение 40 минут, то это может стать проблемой. Если не уделять внимания рефакторингу с самого начала и не стараться делать тесты лаконичными и читаемыми, то в результате у вас будет несколько сотен тестов, которые не сможет поддерживать и читать никто, кроме тестировщика, который их пишет. Есть хорошее правило, которое сформулировал Джерард Месарош в своей книге: если какая-то строка кода напрямую не относится к тесту, её не должно быть в тесте! Нужно стремиться делать код максимально читаемым, скрывая сложную логику инициализации и уничтожения объектов, скрывая данные, которые напрямую не используются в тесте и не влияют на результат.

Длительное прохождение всех тестов тоже может стать проблемой. В идеале интеграционные тесты надо запускать так часто, как только возможно. Как только один из программистов закончил реализацию некой функциональности, в идеале нужно сразу запустить все интеграционные тесты и убедиться, что в существующих сценариях работы пользователя ничего не сломалось. Если что-то пошло не так, то можно быстро исправить ошибку, пока программист ещё держит в голове задачу, над которой только что работал. И только после того, как все существующие тесты проходят успешно, можно приниматься за написание новых тестов.

А теперь представьте, что все ваши тесты длятся, например, 6 часов. Естественно, если программист что-то сломал в существующем коде, то он узнает об этом в лучшем случае на следующий день, когда он уже вовсю работает над новой задачей. В этом случае может помочь написание нескольких интеграционных тестов, которые по максимуму охватывают весь цикл работы пользователя. Этот небольшой набор тестов можно запускать очень часто и проходят они не так долго. Если некоторые тесты из этого набора перестанут проходить, то можно будет примерно выяснить, в какой части сценария работы пользователя произошла ошибка, локализовать её. После этого можно будет запустить все тесты, имеющие отношение к этому участку кода, и точно сказать, где же ошибка. В любом случае, это займёт намного меньше времени, чем прогон абсолютно всех тестов.

Надеюсь, что я достаточно полно раскрыл тему интеграционного тестирования и убедил многих из тех, кто прочитал эту статью, внедрить что-то подобное у себя.

Теги:
рубрика C#