Pull to refresh

От велосипеда к…

Reading time 9 min
Views 17K
Привет!

Этот небольшой очерк адресован 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, приложенном к проекту.

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(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# проект. Но! Все это имеет смысл, если базовая функциональность нужна. Всегда рад пулл-реквестам!

До встречи!

Tags:
Hubs:
+14
Comments 11
Comments Comments 11

Articles