Как правильно запускать браузер, часть 1
На конференции SeleniumCamp, состоявшейся в Киеве в феврале 2010 года, я проводил мастер-класс по оптимизации скорости выполнения тестов, разработанных с использованием инструмента Selenium. И самый первый совет, который я дал, вовсе не касается оптимизации самих тестов. Я предложил обратить внимание на то, как запускается браузер, потому что при неудачной конфигурации время, которое тратится на запуск и останов браузера может на порядок превышать “полезное” время выполнения тестов.
Да, при проектировании тестов для автоматизации есть некоторые особенности, тесты надо строить иначе, чем для ручного выполнения. Но иногда тестировщику автоматизатору приходится думать не только о том, как спроектировать тесты, но и об инфраструктуре для их выполнения. И механизм запуска браузера при автоматизации тестирования веб-приложений является важным элементом этой инфраструктуры.
Я разработал для себя универсальный механизм запуска браузера для инструмента Selenium, который позволяет достаточно гибко управлять различными способами использования браузера в зависимости от потребностей – один браузер на все тесты либо перезапуск перед каждым тестовым методом, автоматический останов браузера после завершения выполнения тестов, автоматический перезапуск браузера при сбоях или после заданного количества использований, а также в случае переключения на другой тип браузера или другой Selenium-сервер, чтение параметров запуска из конфигурационного файла или передача параметров из программного кода.
Но прежде чем рассказать про реализацию этого механизма и отдать его в широкое использование, я хочу изложить свою точку зрения на очень важный, как мне кажется, вопрос:
Как правильно запускать браузер?
При рассмотрении будем учитывать следующие характеристики тестового набора:
- производительность – общее время выполнения всех тестов,
- устойчивость к сбоям тестов – возможность продолжения выполнения тестов после сбоя отдельного теста,
- устойчивость к сбоям браузера – возможность продолжения выполнения тестов после сбоя браузера,
- простота локализации дефектов.
Мы оценим относительно этих характеристик по пятибалльной шкале, а потом построим интегральную оценку для следующих способов запуска браузера:
- отдельный браузер для каждого теста,
- общий браузер для группы тестов,
- общий браузер для всех тестов.
Конечно, первый и третий способы можно считать просто частными случаями более общего второго способа – в первом случае каждая группа состоит из единственного теста, а в третьем случае у нас есть одна большая группа, включающая в себя все тесты. Но я хочу показать, что в данном случае оптимальными являются как раз не эти крайние случаи.
Поэтому правильно было бы поставить вопрос следующим образом: какого размера должны быть группы тестов, для каждой из которых запускается отдельный браузер, чтобы достигалось оптимальное соотношение вышеперечисленных характеристик.
Производительность
Очевидно, что запуск отдельного браузера для каждого теста – это наихудший способ с точки зрения производительности.
На машине, где я сейчас пишу статью, Selenium тратит от 10 до 30 секунд на то, чтобы запустить браузер Firefox 4. При этом тесты я делаю достаточно мелкими, так что среднестатистический тест выполняется не более 10 секунд. То есть “чистое” время выполнения тестов составляет в лучшем случае не более 50%, а в худшем – менее 25% общего времени выполнения тестового набора.
Это же просто расточительство какое-то! За то же время можно было бы выполнить в 4 раза больше тестов без увеличения мощности тестового стенда!
Вы можете спросить – почему браузер так долго стартует? Полминуты это слишком много для современного браузера. Это правда. Однако дело в том, что Selenium сначала готовит для браузера чистый временный профиль, и только потом запускает браузер с этим профилем. Именно эта процедура подготовки профиля занимает так много времени, особенно если учесть, что я обычно предпочитаю иметь в этом профиле пару-тройку полезных для отладки тестов плагинов, таких как Firebug и Web Developer Toolbar. После того, как профиль подготовлен, достаточно быстро стартует браузер, потом в нем столь же быстро выполняются тесты, а затем браузер останавливается, профиль удаляется и надо всё начинать сначала.
Ещё один источник снижения производительности – лишние действия по подготовке тестовой ситуации, которые нужно выполнить после запуска браузера, такие как, например, вход в систему (login) и переход на нужную страницу, при выполнении тестов без перезапуска браузера эти действия если не полностью исчезают, то хотя бы минимизируются. Для медленно работающего приложения это время может быть не меньше, чем время старта браузера.
В общем, этот способ за производительность получает твёрдый кол, то есть единицу.
Полной противоположностью ему является использование одного браузера для всех тестов. Все лишние действия сведены к минимуму. Профиль браузера формируется один раз в самом начале. Если удачно определить порядок выполнения тестов, можно сократить количество действий по входу в систему и выходу из неё (логинимся, выполняем все тесты для одной роли, потом выходим, логинимся с другой ролью, выполняем тесты для неё, и так далее). Иногда можно даже избавиться от “холостых” проходов, если следующий тест начинает выполнение на той странице, на которой завершился предыдущий.
Короче говоря, здесь с производительностью всё хорошо, однозначно высший балл.
Если теперь обратиться к общему случаю, можно сделать вывод, что производительность падает с уменьшением размера группы – чем чаще приходится перезапускать браузер, тем больше на это уходит времени и тем сильнее увеличивается общее время выполнения тестов.
Ставим условно тройку, но помним, что при уменьшении количества тестов в группе она может превратиться в двойку, а при увеличении – в четверку.
И тогда возникает закономерный вопрос – стоит ли делать маленькие группы? Что мы получаем в качестве компенсации за снижение производительности? Давайте посмотрим на другие характеристики, может быть они помогут дать ответ на этот вопрос.
Устойчивость к сбоям тестов
Под “сбоем” теста я подразумеваю здесь такую ситуацию, когда тест после своего завершения оставил сиситему не в том состоянии, в котором ожидал тестировщик, а в некотором другом. Например, тестировщик предполагал, что все тесты будут завершаться возвратом на главную страницу приложения, и поэтому спроектировал тесты исходя из этого предположения. Если теперь некоторый тест после завершения не вернется на главную страницу, у последующих тестов могут возникнуть проблемы, поскольку предусловие для их выполнения нарушено, появляются так называемые “наведенные ошибки”.
Да, иногда “сбои” являются следствием неуспешного завершения теста (test failed) вследствие ошибки в тестируемом приложении или в самом тесте. При этом тест не отрабатывает до конца, его выполнение обрывается где-то посредине и тест “бросает” систему в каком-то промежуточном состоянии.
Но может быть и так, что тест отрабатывает до конца, все проверки, которые тестировщик счел нужным запрограммировать, проходят успешно, но результирующее состояние системы всё таки отличается от ожидаемого (просто тест не содержит соответствующих проверок и это остается незамеченным). Такую ситуацию я также буду называть “сбоем теста”.
По устойчивости к сбоям тестов способ запуска отдельного браузера для каждого теста показывает замечательные результаты – восстановление исходного состояния гарантировано, независимо от того, как завершился предыдущий тест.
Однако высшую оценку мы ему не поставим, ограничимся четверкой, потому что гарантии распространяются только на клиенсткую часть. Если тест оставил в “плохом” состоянии сервер, перезапуск браузера, увы, не поможет.
Поэтому тестировщику всё равно нужно думать о том, как аккуратно выполнить зачистку независимо от успешности или неуспешности завершения теста.
А ещё лучше – спроектировать тесты так, чтобы у них не было предусловий, чтобы они могли стартовать из любого состояния, чтобы каждый тест пытался самостоятельно привести систему в нужное состояние перед началом выполнения основной части теста.
И тогда становится ясно, что перезапускать браузер не нужно, нет необходимости переинициализировать систему, тесты сами позаботятся о создании для себя подходящих условий. Если тесты сами по себе устойчивы к сбоям, тогда теряется главное преимущество способа запуска каждого теста в своем браузере.
Таким образом, второму и третьему способу поставим оценку условно – если тесты спроектированы хорошо, тогда они тоже получают четверку, а для плохих тестов, которым требуется изоляция, эти способы оценим на два балла по устойчивости к сбоям тестов.
Устойчивость к сбоям браузера
Здесь опять некоторое преимущество имеет способ запуска отдельного браузера для каждого теста – если браузер “зависнет” или “упадет”, это окажет негативное влияние только на единственный тест, который выполнялся в этом браузере.
Проблемы могут подстерегать тестировщика в случае, если браузер “зависает” слишком часто, тогда количество неработающих, но не завершившихся процессов будет возрастать, что вскоре приведет к полной неработоспособности компьютера, на котором выполняются тесты. Но на практике такая ситуация встречается крайне редко, поэтому даже не будем снижать за это балл, поставим высшую оценку.
А что будет, если все тесты или группа тестов должны выполняться в одном экземпляре браузера, а он “упал”?
Всё зависит, как и в предыдущем разделе, от того, насколько хорошо спроектированы тесты с точки зрения наличия предусловий. Если тесты могут продолжить выполнение из любого состояния, достаточно просто перезапустить браузер после сбоя, так что пострадает только единственный тест, на котором браузер “упал”.
Следовательно, оценка опять получается условная – если тесты спроектированы правильно, и если имеется механизм перезапуска браузера после падения, второй и третий способы тоже становятся идеально устойчивы к сбоям браузера и тоже получают пятерку. Ну а если тесты не могут продолжить выполнение после перезапуска браузера, тогда способ запуска всех тестов в одном браузере получает единицу, а выделение отдельного браузера для группы тестов в зависимости от размера группы получает оценку от 2 до 4 баллов, потому поставим условную тройку.
Простота локализации обнаруженных дефектов
И вновь способ запуска отдельного браузера для каждого теста в небольшом выигрыше, и для этого есть целых две причины.
Во-первых, для локализации дефекта иногда требуется несколько раз повторно прогнать тест, чтобы понять причину дефекта. Конечно, проще всего это сделать при условии, что выполнять нужно ровно один тест, а не целую группу. Но как и ранее, если тесты спроектированы так, что могут выполняться не только в группе, но и каждый по отдельности – проблема исчезает. Даже если тест “провалился” в составе группы, ничто не мешает впоследствии выполнить его несколько раз в изоляции. Так что опять оценка зависит от того, насколько хорошо написаны тесты, поэтому поставим равные оценки – троечки, потому что локализация дефектов для тестов на уровне пользовательского интерфейса вообще штука достаточно трудоемкая.
Во-вторых, иногда бывает так, что для локализации дефекта всё таки нужно выполнять группу тестов целиком, потому что при выполнении в составе группы тест завершается неуспешно, а если его выполнить в изоляции – всё работает без ошибок. Программисты не любят такие дефекты, их локализация сложна, для воспроизведения нужно прогонять целую серию тестов, а через пользовательский интерфейс они выполняются медленно.
Но давайте взглянем на это с другой стороны – это же замечательно, что такой дефект вообще удалось обнаружить! Если бы все тесты выполнялись в изоляции, мы бы вообще не имели шанса выявить проблему, которая является следствием влияния одной тестируемой функции на другую. Так что это отнюдь не минус, а очень даже жирный плюс! Поэтому оценку снижать не будем.
Подведение итогов
Итак, давайте посмотрим, каких оценок удостоились наши три способа запуска тестов. В представленной ниже таблице в качестве основных оценок указаны те, которые получены при “правильном проектировании тестов”, а в скобках указана оценка для “плохо спроектированных тестов”.
Все тесты вместе | Группы тестов | Каждый тест отдельно | |
Производительность | 5 | 3 | 1 |
Устойчивость к сбоям тестов | 4 (1) | 4 (2) | 4 |
Устойчивость к сбоям браузера | 5 (1) | 5 (3) | 5 |
Простота локализации дефектов | 3 | 3 | 3 |
Какие выводы можно сделать из представленной сравнительной таблицы?
- Первый и самый главный вывод – тесты надо проектировать правильно :) , чтобы их можно было использовать с любым из способов запуска браузера.
- Если выполнено условие, упомянутое в первом пункте, лучше все тесты запускать в одном браузере, не изолировать их друг от друга, это обеспечивает существенно более высокую скорость выполнения тестов благодаря отсутствии накладных расходов на перезапуск браузера.
- При удачном (или неудачном? нет, всё таки удачном) стечении обстоятельств отсутствие изоляции позволяет найти трудноуловимые дефекты, связанные с особенностями взаимодействия тестируемых функций при их последовательном использовании.
Что же представляют собой “правильно спроектированные тесты”, какими особенностями должны обладать тесты и/или тестовый фреймворк, чтобы обеспечить возможность выполнения тестов как в изоляции, так и в составе произвольной группы?
- Тесты не должны иметь предусловий. Если для выполнения теста необходимо создать определенную тестовую ситуацию, тест должен сам позаботиться об этом, не полагаясь на то, что это сделают ранее выполненные тесты.
- Необходим механизм отслеживания состояния браузера и перезапуска в случае сбоя. Этим должен, конечно, заниматься фреймворк, а не тесты.
- Необходим механизм принудительного перезапуска браузера по требованию теста – если тест не может создать нужную ситуацию без перезапуска браузера, нужно дать ему последний шанс.
Так что же, неужели всегда лучше выполнять все тесты в одном браузере? Бывают ли ситуации, когда “правильнее” запускать тесты небольшими группами или даже выделять отдельный браузер для каждого теста? Конечно бывают, но я хочу подчеркнуть, что эти потребности не связаны с устойчивостью к сбоям и простотой локализации тестов, с теми “причинами”, которые я чаще всего встречал в блогах и статьях в качестве обоснования необходимости изоляции тестов.
Вот несколько причин выделения отдельного браузера для группы тестов, с которыми мне приходилось сталкиваться в своей практике (список, конечно, не претендует на полноту):
- параллельный запуск, каждая группа выполняется в своем браузере,
- запуск в разных браузерах или в одном и том же браузере, но с разными настройками – группа тестов выполняется сначала в одном браузере, потом в другом (да, можно решить это и другим способом – либо разные браузеры работают параллельно, либо сначала все тесты выполняются в одном браузере, потом в другом, но иногда требуется и выполнение сначала полностью одной группы, а потом переход к другой),
- профилактика сбоев браузера – например, Firefox знаменит тем, что при выполнении большого количества тестов он захватывает много оперативной памяти (вероятно, из-за наличия “утечек”), начинает работать медленнее и может совсем “упасть”, поэтому его необходимо периодически перезапускать,
- необходимость эмуляции “нового пользователя”, который никогда ранее не заходил на сайт и не пользовался приложением (можно пытаться чистить куки и кеш, но проще перезапустить браузер),
На этом теоретическая часть закончена, вторая часть статьи будет содержать описание (и реализацию) универсального механизма запуска браузера, который годится для любого способа, умеет отслеживать состояние браузера и автоматически перезапускать его, а также обеспечивает возможность перезапуска браузера по требованию.