Я хочу рассказать о созданной нами системе автоматизированного тестирования. Система в моем понимании это не только код, но еще железо, процессы и люди.
Я отвечу на вопросы: Что тестируем? Кто этим занимается? Зачем это все происходит? Что у нас есть?
А затем расскажу как все работает: опишу круги тестирования — с первого по девятый.
Что?
Наш продукт — корпоративное web-приложение Service Desk, написано на java.
Кто?
Я — лид группы автоматизированного тестирования; программисты код которых тестируем; ручные тестировщики, рутину которых мы искореняем; менеджеры верящие, что если тесты прошли, то все не так уж и плохо.
Зачем?
Цель моей группы — уберечь продукт от регрессионной спирали смерти.
Задача группы — необнаружение дефектов максимумом интересных способов с минимальных количеством ручного труда.
Что у нас уже есть?
900 коротких и не очень сценариев использования приложения закодированых в тесты.
CI Jenkins на шести серверах, три СУБД, два семейства ОС и три браузера под которые пишем продукт.
Как это работает?
Круг первый — сценарий теста.
Мельчайшая единица — тест, начинается со сценария.
- Заголовок теста — коротко и ясно описывает, что мы проверяем.
- Ссылка на постановку по которой написан тест; если ее нет, то кейс лишь игра воспаленного воображения тестировщика, у нас корпоративное ПО, мы не шутки шутим.
- Сценарий теста на русском, описывающий три фазы теста — подготовка, выполнение действия и верификацию.
Note: Сценарий — не перевод с java на русский, напротив, сценарий первичен, причем на написание качественного кейса уходит больше времени, чем на его кодирование. В нашем случае он, как правило, пишется не тем человеком, который кодирует тест.
Первая фаза теста — подготовка тестирующей системы.
Catalog catalog = DAOCatalog.create(true, false);
catalog.set(Catalog.TITLE, “Я хочу чтоб в этом тесте у объекта было такое название”)
DSLCatalog.add(catalog);
CatalogItem item = DAOCatalogItem.create(catalog);
DSLCatalogItem.add(item);
Концепция — есть модель системы, описываемая методами из DAO; есть методы с префиксом DSL которые приводят тестируемую систему к состоянию, описываемому моделью.
Методы из DSL меняют приложение через API системы, в нашем случае это restfull сервис. Всю первую фазу теста проводим через API.
Note: Правило — для каждого закодированного тестового действия написать DSL, чтоб в дальнейшем выполнять его через API.
Модель — мапа строк, мы стараемся избегать излишнего усложенения тестирующей системы.
Вторая фаза теста — тестовое действие с использованием selenium, через браузер.
DSLCatalog.goToCard(tester, item.get(CatalogItem.PARENT_CODE));
tester.clickElement(ADD_ELEMEMT);
DSLForm.assertFormAppear(tester, DSLForm.DIALOG);
tester.sendKeys(DSLForm.TITLE_INPUT, item.get(CatalogItem.TITLE));
tester.sendKeys(DSLForm.CODE_INPUT, item.get(CatalogItem.CODE));
tester.clickElement(DSLForm.SAVE_ON_FORM);
DSLForm.assertFormDisappear(tester, DSLForm.DIALOG);
Note: Также для обеспечения тестируемости каждый новый элемент интерфейса в системе имеет свой id уникальный в рамках страницы. Поэтому наши xpath выглядят так:
"//*[@id='description-value']"
или
"//*[@id='description-input']"
А не так:
"/html/body/table/tbody/tr/td[6]/table/tbody/tr[4]/td/div"
Решается класс проблем с изменениями верстки.
Третья фаза — проверка или верификация. Хотя бы раз выполняем ее через интерфейс:
Assert.assertEquals("Название отображается неверно в карточке шаблона отчета", template.get(ReportTemplate.TITLE), tester.getTextElement(DSLForm.TITLE_VALUE));
А затем можно проверять уже через API.
ModelMap map = SdDataUtils.getObject(sc.get(Bo.METACLASS_FQN), Bo.UUID, sc.getUUID());
String message = String.format("Атрибут БО с uuid-ом '%s' имеет другое значение.", sc.getUUID());
Assert.assertEquals(message, team.get(Bo.TITLE), SdDataUtils.getMapValue(map, "responsibleTeam").get("title"));
Note: У нашего приложения асинхронный интерфейс; в сообществе автоматизаторов много пишут о проблемах с ожиданием появления элементов, мы же заказали у программистов счетчик асинхронных запросов. Значительно ускоряет процесс тестирования.
Вторая и третья фаза могут проходить без использования браузера, как например почти сотня тестов на API.
Note: У кого-то должна возникнуть мысль: а не слишком ли много внимания уделяется тестированию через UI и на поднятом приложении?
Отвечу: особенность нашего ПО — высокая сложность не отдельных функций, но бизнес-логики. Практически каждый тест требует наличия БД и большого контекста. В таком случае unit-тесты не намного выигрывают по времени и значительно проигрывают по сложности написания.
Круг второй. Окружение теста.
Перед каждым запуском тестирующей системы:
- проверяем доступность системы и возможность в нее залогиниться
- проверяем глобальные настройки системы и если нужно устанавливаем нужные значения
Перед каждым классом с тестами
- перезапускаем браузер
- проверяем доступность системы
После каждого теста:
- удаляем все созданные им объекты — это необходимо для поддержания скорости работы системы и изоляции тестов. При создании объекты помещаются в очередь и удаляются в обратном порядке.
Круг третий — иерархия тестов.
- Один тест — одно действие. Исключений не больше 5%
- Тесты объединены в классы по 3-10 штук, они тестируют бизнес-требование
- 5-15 классов объединены в пакеты. Пакет тестирует сущность или аспект работы приложения
- Пакетов 15
Note: Javadoc, собирается командой
mvn -f sdng-inttest/pom.xml javadoc:test-javadoc -e
Помогает ручным тестировщикам и ПМам узнать, какие есть тесты, позволяет не искать нужное в web-интерфейсе git и не устанавливать IDE.
Корень выглядит так:
Круг четвертый — организация проекта тестирующей системы.
60 000 строк кода тестирующей системы это
- Код тестов
- Утилитарные методы домена: код DSL(работа с тестируемой системой) и DAO(работа с моделями)
- Утилитарные методы не привязанные к тестируемому приложению — работа со строками, файлами, JSON и т.п.
- Ядро тестирующей системы: логи, скриншоты, очистка, интерфейс к webdriver, механизм исключений.
Note: Так выглядят настройка браузера для тестов, надеюсь кому-нибудь будет полезна:
FirefoxProfile/** * Открыть Firefox браузер. * http://code.google.com/p/selenium/wiki/FirefoxDriver * @return возвращает экземпляр класса FirefoxDriver, реализующий интерфейс WebDriver. * @exception WebDriverException если невозможно открыть браузер. */ private WebDriver openFirefox() { FirefoxProfile firefoxProfile = new FirefoxProfile(); //Память на вкладки firefoxProfile.setPreference("browser.sessionhistory.max_total_viewer", "1"); //Значение 3 не просто так, иначе не работает авторефреш firefoxProfile.setPreference("browser.sessionhistory.max_entries", 3); firefoxProfile.setPreference("browser.sessionhistory.max_total_viewers", 1); firefoxProfile.setPreference("browser.sessionstore.max_tabs_undo", 0); //Асинхронные запросы к серверу firefoxProfile.setPreference("network.http.pipelining", true); firefoxProfile.setPreference("network.http.pipelining.maxrequests", 8); //Задержка отрисовки firefoxProfile.setPreference("nglayout.initialpaint.delay", "0"); //Сканирование внутренним сканером загнрузок firefoxProfile.setPreference("browser.download.manager.scanWhenDone", false); //Анимация переключения вкладок firefoxProfile.setPreference("browser.tabs.animate", false); //Автоподстановка firefoxProfile.setPreference("browser.search.suggest.enabled", false); //Анимация гифок firefoxProfile.setPreference("image.animation_mode", "none"); //Резервные копии вкладок firefoxProfile.setPreference("browser.bookmarks.max_backups", 0); //Автодополнение firefoxProfile.setPreference("browser.formfill.enable", false); //Убрал дисковый кеш и кеш в памяти //firefoxProfile.setPreference("browser.cache.memory.enable", false); firefoxProfile.setPreference("browser.cache.disk.enable", false); //Сохранять файлы без подтверждения в tslogs firefoxProfile.setPreference("browser.download.folderList", 2); firefoxProfile.setPreference("browser.download.manager.showWhenStarting", false); firefoxProfile.setPreference("browser.download.dir", new File("").getAbsolutePath()); firefoxProfile.setPreference("browser.helperApps.neverAsk.saveToDisk", "application/xml,application/pdf,application/zip,text/plain,application/vnd.ms-excel"); return new FirefoxDriver(firefoxProfile); }
А еще у нас есть такая замечательная штука, как unit-тесты на ядро тестирующей системы.
Note: Тестирующая система, живет уже полтора года, пережила переезд из svn в git, 300 коммитов, поэтому 100 unit тестов жизненно необходимы
Два основных сценария использования — запуск из Jenkins и из eclipse
Алгоритм работы с конфигурацией запуска:
- Настройки тестирующей системы переданные из maven — самого высокого приоритета. Их пишем в конфиг. Если конфига нет, то создаем.
- Остальные настройки берем из конфига. Если конфига нет, то по умолчанию.
- Таким образом разработчики приложения и тестов настраивают свой персональный конфиг, а в CI все нужные параметры передаем через maven.
allowscreenshot=true
needinit=true
superlogin=naumen
allowsaveconfig=true
clickertime=3600000
testerpassword=atpassword
testerlogin=atlogin
superpassword=тутявсежезаменилтекст
needdelete=true
webaddress=http://localhost:9090/sd/
browsertype=firefox
Note: У ТС все по взрослому — есть свои постановки и сценарии использования.
“Я как автотестировщик хочу иметь возможность запускать тестирующее приложение на локальном компьютере, чтобы произвести определенные тесты на локальном стенде или клиентском, для дальнейшего анализа полученных данных, или для разработки новых тестов.”
“Я как разработчик хочу, чтобы ТС проверяла проект после каждого изменения, для своевременного выявления проблем и ошибок.
И так далее.
Балансировку тестов для их распараллеливания мы выполняем достаточно некрасивым образом.
<profile>
<id>smoke-snap2-branch</id>
...
<execution>
<id>integration-tests</id>
<configuration>
<excludes>
<exclude>all</exclude>
</excludes>
<includes>
<include>**/selenium/**/advlist/*Test.java</include>
<include>**/selenium/**/bo/*Test.java</include>
<include>**/selenium/**/advimport/*Test.java</include>
<include>**/selenium/**/user/*Test.java</include>
<include>**/selenium/**/rights/*Test.java</include>
</includes>
...
</profile>
Есть несколько таких профилей, по которым вручную разбиты пакеты.
Круг пятый. Управление исходным кодом.
Мы используем эту модель ветвления git как наиболее адекватную нашим потребностям.
Note: Тесты находятся в одном месте с кодом приложения. Таким образом, отцепляя ветку, разработчик отцепляет и код тестов, это обеспечивает синхронизацию тестов и приложения. Даже к этому очевидному решению мы пришли не сразу.
Круг шестой. Отдельная сборка в CI Jenkins.
Сборка начинается с описания. Я считаю, что правила должны находиться как можно ближе к месту их применения. Тут же — полезные ссылки.
Полезные плагины для настройки сборок:
- Clone Workspace, позволяющий избежать рассинхронизации, когда между компилированием кода в родительской сборке и запуском тестов в дочерней кто-то сделал коммит.
- Xvfb — виртуальный экран.
Запуск тестов:
export DISPLAY='localhost:'$DISPLAY_NUM'.0';
mvn verify -P war-deploy,$TEST_PROFILE -Dext.prop.dir=$WORKSPACE/$PROP_DIR -Dwebaddress=http://localhost:$DEPLOY_PORT/sd -Dselenium.deploy.port=$DEPLOY_PORT -Dcargo.tomcat.ajp.port=$ARJ_PORT -Dcargo.rmi.port=$RMI_PORT
где профиль war-deploy — поднятие приложения с помощью maven-cargo-plugin
Послесборочные операции.
- Заархивировать артефакты, сохранить отпечатки
- Jabber и email notification
- Blame Upstream Committers позволяет отправлять письма коммитерам родительской сборки.
Круг седьмой. Иерархия сборок.
Группа сборок, предоставляющая сервис программистам, у нас называется веткой.
Родительская сборка — компиляция проекта.
Дочерние сборки второго уровня — unit-тесты для postgresql, mssql, oracle; статический анализ кода (CPD, PMD, findbugs); сборка war файла (под один браузер для экономии времени) для UI тестов.
Третий уровень — UI тесты в три потока и тесты на миграции.
Note: Сборка тестирования миграций — простой и эффективный тест, в котором мы берем бекап древней базы с данными и поднимаемся на последнем war файле. Позволяет удостовериться, что пройдут все миграции данных и приложение поднимется на непустой БД.
Восьмой круг, уровень системы непрерывной интеграции
Наша CI — это 4 ветки, в которых программисты могут прогонять тесты, запуская вручную и указывая свой branch. И одна main ветка, запускающаяся по коммиту в develop, от остальных отличающаяся наличием сборок тестирования на всех поддерживаемых СУБД. Не раньше, чем две недели назад каждая ветка располагалась на отдельном сервере (мы выбрали EX4, так как нам не нужна надежность, но критична скорость процессора; эксперименты показали что оптимально — три ноды Jenkins на сервер). После перенастройки виртуальных экранов, портов и настроек СУБД у нас появилось общее пространство нод.
Jenkins promoted builds plugin, позволяющий рисовать красивые звездочки сборкам, все дочки которых прошли без упавших тестов. Он оказался полезней, чем три сотни тестов через интерфейс или тысяча unit-тестов. До него отдельные несознательные ПМы выпускали релизы, невзирая на наличие упавших тестов, уговоры и угрозы не помогали. Но как только у каждого билда появились звездочки — сработала первая сигнальная система и раз в две недели лейтмотив дня — ждем двух звезд. Это вылилось в повышенные требования к скорости исправления ошибок программистами и к качеству кода тестирующей системы — случайные срабатывания теперь стоят дороже.
Сборка, использующая Performance Plugin — отслеживание производительности типовых операций, позволяет достаточно быстро обнаружить самые грубые ошибки связанные с производительностью системы.
В разработке сборка, автоматически проводящая полноценное нагрузочное тестирование. Но это тема отдельной статьи.
Note: Еще несколько полезных штук:
- Parameterized Trigger Plugin — если у вас сложная иерархия сборок.
- JobConfigHistory Plugin — вместе с LDAP творит чудеса.
- Configuration Slicing Plugin — если у вас больше 50 почти разных сборок.
- Rebuild Plugin просто полезная штука.
- The Continuous Integration Game plugin — моя группа всегда выигрывает, так как мы только и делаем, что пишем тесты.
Девятый круг, люди и процессы.
Самое сложное в автоматизированном тестировании — добыть хороший кейс. На это уходит до половины времени. Основной поставщик — ручные тестировщики, их кейсы мы съедаем и просим еще, но у них масса другой интересной работы. Поэтому пишем тесты на дефекты, и вообще каждая выполненная таска в JIRA проходит через мой пристальный взгляд — можно ли на нее написать тест? Еще у нас есть Emma Plugin и три сборки сборки подсчета покрытия — для uint-тестов, для UI тестов и суммарное. По этим отчетам мы пишем тесты на API системы. Но для остальной функциональности отчет по покрытию ценности не имеет — специфика нашего ПО такова, что нужно ориентироваться на покрытие тестами не кода, но требований.
Разработка фичи в контексте автоматизированного тестирования выглядит так:
Отцепить ветку в git. Написать код фичи. Прогнать тесты в ветке. Исправить тесты в ветке. Получить две звезды. Слить с develop.
Принцип, замедливший скорость разработки, повысивший готовность кода к релизу и сэкономивший массу времени моей группе:
Моральным правом на коммит обладает программит, сборка тестирования в ветках которого получила две звезды.
Если программист отцепился от коммита, в котором все тесты проходили, то и его коммит должен обладать таким же свойством.
На первых порах принцип был подкреплен revert'ом коммитов и запретом коммитов в develop.
Такой процесс разработки наложил ограничение на суммарное время прохождения всех тестов — 2 часа. У нас нет ночных тестов и тестов запускающихся на выходных, абсолютно все тесты проходят максимум за 2 часа. Группа автоматизированного тестирования последние полгода работала примерно в таком режиме:
Написать тестов до 2 часов — докупить серверов и распараллелить — написать тестов до 2 часов — оптимизировать код тестов — написать тестов до 2 часов — поменять сервера на более быстрые — написать тестов ...
Послесловие
Все, о чем я рассказал — не моя заслуга. Мой вклад в код тестов — треть строк. В CI — четверть конфигов. Над системой тестирования и непрерывной интеграции работали два десятка людей — от техподдержки и тестировщиков до руководителя разработки и аналитиков.
Хочу услышать от вас не только вопросы, но и мудрые советы, полезные настройки и классные идеи. Только помните о моей роли в проекте — я могу что угодно менять в коде, многое в CI, кое-что в процессах. Людей я изменить не в силах.
Если найдется желающий не только дать ценное указание, но например научиться все это делать самому — или научить меня, как нужно правильно работать — добро пожаловать в личку, я ищу коллегу.
Автор: Wolonter