От велосипеда к…
Привет! Этот небольшой очерк адресован QA — специалистам и в большей степени разработчикам, которые привлечены к автоматизации тестирования вэб и мобильных приложений. Те, кто просто интересуется open source' ом — тоже welcome.
Здесь я хочу развить мысли, высказанные год назад в статье «Про Selenium и один «велосипед».
План:1. Основные фичи (краткий обзор)2. Как развивалось (лирическое отступление)3. Заключение.
Вы можете сразу ознакомиться с решением. Но если интересно сначала прочитать статью — поехали!
1. Основные фичи (краткий обзор)Как вы уже поняли, решение написано на Java 8. Основными компонентами являются Selenium (для обеспечения взаимодействия с десктопными браузерами), Appium (java-client, для обеспечения взаимодействия с мобильными браузерами и приложениями).
Если говорить о принципе и способе работы с Selenium Webdriver, то он в чем-то похож на то, что предлагают такие решения как Html elements от Яндекс и Thucydides. Но все это несколько пересмотрено.
То, что получилось, у меня вызывает ассоциации с каком-то членистоногим существом. Учитывая, что Selenium и Appium находят свое применение в автоматизации тестирования + мы все знаем перевод слова bug, то получилось такое название для решения — Arachnidium (лат. «паукообразный»).
Итак.
Кроссбраузерность Я думаю этот пункт подразумевается как нечто само собой разумеющееся. Но упомянуть надо.Поддерживаются: — Firefox; — Chrome; — Internet Explorer; — Safari; — PhantomJS.— Удаленный запуск перечисленных выше браузеров.От поддержки HtmlUnitDriver и OperaDriver пришлось отказаться. Первый не является наследником RemoteWebDriver и его поддержка чревата костылями, второй устарел (например, он не запускает Opera на Windows 8/8.1). Но есть актуальная замена: — Chrome для Android. Чуть позже хочу добавить поддержку родного браузера Android и Chromium— Mobile Safari для iOS
Поддержка автоматизации взаимодействия с UI нативных и гибридных мобильных приложений. Это возможно за счет интенсивного использования возможностей Appium.
Но, даже не это, на мой взгляд, самое интересное.
Возможность моделировать пользовательский интерфейс приложения как по частям так и в целом. Подробно я описал эти принципы тут, тут и тут (по английски). Но чтобы не отвлекать читателей, я пожалуй кое-что процитирую, что-нибудь по-эффектнее.
Для своих тестов на Android я использую приложение BBC News. Его UI очень похож на UI сайта по визуальному составу элементов. Предположим, что нужно протестировать как сайт, так и приложение для Android. Тогда можно описать пользовательский интерфейс так.
Список и просмотр новостей:
Код под катом
/**
* Imagine that we have to check browser and Android versions
* How?! See below.
*/
@IfBrowserURL (regExp = «http://www.bbc.com/news/»)
@IfMobileContext (regExp = «NATIVE_APP»)
@IfMobileAndroidActivity (regExp = «HomeWwActivity»)
public class BBCMain extends FunctionalPart
@FindBy (className = «someClass1»)
@AndroidFindBy (id = «bbc.mobile.news.ww: id/articleWrapper»)
private List
@FindBy (className = «someClass2») @AndroidFindBy (id = «bbc.mobile.news.ww: id/articleWebView») private RemoteWebElement currentArticle;
@FindBy (className = «someClass3») @AndroidFindBy (id = «bbc.mobile.news.ww: id/optMenuShareAction») private RemoteWebElement share;
@FindBy (className = «someClass4») @AndroidFindBy (id = «bbc.mobile.news.ww: id/optMenuWatchListenAction») private RemoteWebElement play;
@FindBy (className = «someClass5») @AndroidFindBy (id = «bbc.mobile.news.ww: id/optMenuEditAction») private RemoteWebElement edit;
@FindBy (className = «someClass6») @AndroidFindBy (uiAutomator = «new UiSelector ().resourceId» + »(\«bbc.mobile.news.ww: id/optMenuRefreshAction\»)») private RemoteWebElement refresh;
protected BBCMain (Handle context) { super (context); load (); }
@InteractiveMethod public int getArticleCount (){ return articles.size (); }
//some more staff //… }
Формочка выбора новостей по категориями:
Код под катом
/**
* Imagine that we have to check browser and Android versions
* How?! See below.
*/
@IfBrowserURL (regExp = «http://www.bbc.com/news/»)
@IfMobileContext (regExp = «NATIVE_APP»)
@IfMobileAndroidActivity (regExp = «PersonalisationActivity»)
public class TopicList extends FunctionalPart
@CacheLookup
@FindBys ({@FindBy (linkText = «someLink»),
@FindBy (linkText = «someLink2»),
@FindBy (linkText = «someLink2»)})
@AndroidFindBys ({@AndroidFindBy (id =
«bbc.mobile.news.ww: id/personalisationListView»),
@AndroidFindBy (className = «android.widget.LinearLayout»),
@AndroidFindBy (uiAutomator = «new UiSelector ()»+
».resourceId (\«bbc.mobile.news.ww: id/feedTitle\»)»)})
private List
@CacheLookup
@FindBys ({@FindBy (linkText = «someLink3»),
@FindBy (linkText = «someLink4»),
@FindBy (linkText = «someLink5»)})
@AndroidFindBys ({@AndroidFindBy (id =
«bbc.mobile.news.ww: id/personalisationListView»),
@AndroidFindBy (className = «android.widget.LinearLayout»),
@AndroidFindBy (uiAutomator = «new UiSelector ().»+
«className (\«android.widget.CheckBox\»)»)})
private List
@AndroidFindBy (id = «bbc.mobile.news.ww: id/personlisationOkButton») private WebElement okButton;
protected TopicList (Handle context) { super (context); load (); }
//some more staff //… } Тест (самый упрощенный вид):
Android
Код под катом @Test public void androidNativeAppTest () { Configuration config = Configuration .get («android_bbc.json»); Application,?> bbc = MobileFactory.getApplication ( Application.class, config); try { BBCMain bbcMain = bbc.getPart (BBCMain.class); Assert.assertNotSame (0, bbcMain.getArticleCount ()); bbcMain.selectArticle (1); Assert.assertEquals (true, bbcMain.isArticleHere ());
bbcMain.edit ();
TopicList> topicList = bbcMain.getPart (TopicList.class); topicList.setTopicChecked («LATIN AMERICA», true); topicList.setTopicChecked («UK», true); topicList.ok ();
bbcMain.edit (); topicList.setTopicChecked («LATIN AMERICA», false); topicList.setTopicChecked («UK», false); topicList.ok (); } finally { bbc.quit (); } } Браузер (десктопный/мобильный)
Код под катом @Test public void webTest () { Configuration config = Configuration .get («android_some_browser.json»); Application,?> bbc = WebFactory.getApplication ( Application.class, config, urlToBBCNews); //does the same О некоторых вещах я расскажу чуть позже.
Я постарался реализовать универсальную (скорее — условно универсальную) модель, чтобы разработчик фрэймворка для автотестов (а я вижу себя в этой роли, для себя я плохо не сделаю :)) не множил код, а смог сделать его независимым от окружения (я понимаю под этим то, как тест выполняется — в браузере или это нативный контент/html контент гибридного приложения).
Используется дизайн — паттерн Page Object. Мой вариант предполагает, что страницы/скрины могут быть описаны как целиком, так и по частям, если есть повторяющиеся виджеты или наборы элементов. Можно даже заставить целое приложение вести себя как Page Object!
Еще одной особенностью является то, что многие технические нюансы, связанные с необходимостью управления экземпляром WebDriver’а в тех ситуациях, когда одновременно присутствует несколько окон браузера (или контекстов, если мобильное приложение) и часть контента размещена в ifram’ах — автоматизированы. Так что можно полностью сосредоточиться на описании бизнес-логики!
Архитектура. В современных условиях, мое имхо, решает не монолитная архитектура, а модульная или «прозрачная». Я постарался реализовать все так, чтобы можно было использовать как стандартные решения Selenium и Appium (этот способ декорирования элементов в моем решении используется по умолчанию), для которых я постарался предусмотреть удобные способы работы, так и, теоретически — решения сторонних разработчиков.
Здесь примеры:
— совместной работы с HtmlElements от Яндекса. Ссылка.— совместной работы с Selenide от Сodeborne. Ссылка.— использования Thucydides. Ссылка. Кому интересно — отчет для web (GoogleDrive) и отчет для Android (BBC News, виртуальная машина Genymotion, эмулирующая Android-планшет). Приятного просмотра и не забудьте распаковать. Хочу позже сделать похожий сэмпл для Allure.
Способ настройки В данном случае я подразумеваю передачу и хранение параметров для запуска браузеров и мобильных приложений (так задумывалось с самого начала). Подробно описано здесь. Но как уж и быть. Приведу пример.Пусть есть общая настройка, хранящаяся в файле settings.json, приложенном к проекту.
JSON с дефолтными параметрами { «settingA»: { «aValue»:{ «type»: «STRING», «value»: «AAA» } }, «settingB»: { «bValue»:{ «type»: «STRING», «value»: «bbb» } }, «settingC»: { «cValue»:{ «type»: «STRING», «value»: «C» } }, «settingD»: { «dValue»:{ «type»: «STRING», «value»: «D…» } } } И есть файл, названный по другому, содержащий такие данные, которые как бы перекрывают данные из примера выше.
JSON с кастомными параметрами { «settingB»: { «bValue»:{ «type»: «INT», «value»:»1» } }, «settingC»: { «cValue»:{ «type»: «BOOL», «value»: «true» } } } Код ниже
код import com.github.arachnidium.util.configuration.Configuration; import org.junit.Before; import org.junit.Test;
public class DemoTest { Configuration testConfig; private String aGroup = «settingA»; private String bGroup = «settingB»; private String cGroup = «settingC»; private String dGroup = «settingD»;
private String aValue = «aValue»; private String bValue = «bValue»; private String cValue = «cValue»; private String dValue = «dValue»;
@Before public void setUp () throws Exception { testConfig = Configuration.get («src/test/resources/test.json»); }
@Test public void test () {
Object a = Configuration.byDefault.getSettingValue (aGroup, aValue); Object b = Configuration.byDefault.getSettingValue (bGroup, bValue); Object c = Configuration.byDefault.getSettingValue (cGroup, cValue); Object d = Configuration.byDefault.getSettingValue (dGroup, dValue);
System.out.println (a); System.out.println (a.getClass ()); System.out.println (b); System.out.println (b.getClass ()); System.out.println©; System.out.println (c.getClass ()); System.out.println (d); System.out.println (d.getClass ());
System.out.println (); System.out.println (); System.out.println («Showtime! Customized setting see below.»); System.out.println (); System.out.println ();
a = testConfig.getSettingValue (aGroup, aValue); b = testConfig.getSettingValue (bGroup, bValue); c = testConfig.getSettingValue (cGroup, cValue); d = testConfig.getSettingValue (dGroup, dValue);
System.out.println (a); System.out.println (a.getClass ()); System.out.println (b); System.out.println (b.getClass ()); System.out.println©; System.out.println (c.getClass ()); System.out.println (d); System.out.println (d.getClass ()); } } дает такой вывод на консоль
то, что вывела консоль AAA class java.lang.String bbb class java.lang.String C class java.lang.String D… class java.lang.String
Showtime! Customized setting see below.
AAA class java.lang.String 1 class java.lang.Integer true class java.lang.Boolean D… class java.lang.String Таким образом, предполагается наличие общей настройки, в которой указаны дефолтные данные, и набора таких конфигураций, которые частично перекрывают или уточняют эти данные. При этом если данные не указаны в таком кастоме — будет использована общая информация.
Я предполагаю использовать такой механизм для старта браузеров и мобильных приложений. Но, в принципе, он может найти и более широкое применение благодаря своей гибкости и расширяемости.
На этом я заканчиваю свой обзор. Здесь есть и другие интересные вещи. Может быть, о них я расскажу в другой статье или в комментариях к этой.
2. Как развивалось (лирическое отступление). Большую часть своей профессиональной биографии я занимался автоматизацией тестирования десктопного софта, главным образом с помощью Test Complete. Тут было много всего интересного и нетривиального. Но я устал. Потянуло на какое-то творчество.
Позже я узнал про Selenium Webdriver (а кто сейчас не знает?). Сначала были просто эксперименты. Потом стали появляться идеи. Хотя… Я их брал из накопленной практики. Например, такое понятие как Page Object и примеры реализации не вызвали у меня Wow — эффекта. Нечто подобное приходилось делать для десктопных приложений.
Описанный в главе выше эксперимент был прекращен и возобновлен спустя несколько месяцев.
Далее я узнал про Appium. Мне даже довелось поучаствовать в этом проекте! Участие началось спонтанно — с репортинга багов и того, что мне казалось проблемным. Позже появилась клиентская библиотека для java: java-client. Тут вклад серьезнее. Мне удалось реализовать фичи для работы с PageFactory и помочь с редизайном библиотеки, в результате которого появились AndroidDriver и IOSDriver и появилась гибкость, если понадобится добавить поддержку Firefox OS и Windows Mobile. Надеюсь, это помогло многим другим людям по всему миру.
3. Заключение. Буду рад общению в комментариях.Этот эксперимент я довел до такого состояния, что не стыдно сказать, что билды доступны в maven central. Пока я предложил бы с ними поиграть или попробовать автоматизировать какой-нибудь несложный тест-кейс.
Если кто-то найдет баг — это круто! Прошу описать его здесь. А в комментариях было бы здорово почитать критику, идеи, если кто-то похвалит — это тоже хорошо.
На описанном останавливаться не хочется. Есть идея продолжить банкет — реализовать плагины для JUnit, TestNG и Jbehave. Можно сделать плагин, например, для Eclipse IDE, но я с трудом представляю пока его функции. Кроме того — существует пока еще пустой C# проект. Но! Все это имеет смысл, если базовая функциональность нужна. Всегда рад пулл-реквестам!
До встречи!