Привет!
Этот небольшой очерк адресован 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<Handle>{
@FindBy(className = "someClass1")
@AndroidFindBy(id = "bbc.mobile.news.ww:id/articleWrapper")
private List<RemoteWebElement> articles;
@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<Handle> {
@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<WebElement> titles;
@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<WebElement> checkBoxes;
@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, приложенном к проекту.
{
"settingA":
{
"aValue":{
"type":"STRING",
"value":"AAA"
}
},
"settingB":
{
"bValue":{
"type":"STRING",
"value":"bbb"
}
},
"settingC":
{
"cValue":{
"type":"STRING",
"value":"C"
}
},
"settingD":
{
"dValue":{
"type":"STRING",
"value":"D..."
}
}
}
И есть файл, названный по другому, содержащий такие данные, которые как бы перекрывают данные из примера выше.
{
"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(c); 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(c); 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# проект. Но! Все это имеет смысл, если базовая функциональность нужна. Всегда рад пулл-реквестам!
До встречи!
Автор: Sergey_Tikhomirov