Appium для народа

30
Черемушкин Дмитрий инженер по автоматизации тестирования ПО Scalable eCommerce Platform Solutions Обо мне Обо мне 6+ лет в тестировании ПО ручное | автоматизированное настольное | веб | мобильное автоматизация рутинных действий в QA-процессах, интеграция инструментов Черемушкин Дмитрий 1

description

Доклад Дмитрия Черемушкина на SQA Days-15. 18-19 апреля, 2014, Москва. www.sqadays.com

Transcript of Appium для народа

Page 1: Appium для народа

Черемушкин Дмитрий

инженерпо автоматизации тестирования ПО

Scalable eCommerce Platform SolutionsОбо мнеОбо мне

6+ лет в тестировании ПО

ручное | автоматизированное

настольное | веб | мобильное

автоматизация рутинных

действий в QA-процессах,

интеграция инструментов

Черемушкин Дмитрий

1

Page 2: Appium для народа

2

Постановка задачиПостановка задачи

Имеется: фреймворк,основанный на стеке технологий:

+ автоматизированные тесты для eCommerce веб-сайта~ 2000 тесткейсов • Firefox, Chrome, IE • локальный и удаленный запуск

Page 3: Appium для народа

3

1 Запустить имеющиеся тесты(в Android Browser и Mobile Safari)

Разработать новые тесты(мобильная версия сайта + Android-приложение)

2

Необходимо:

на мобильных ОС Android и iOS

Постановка задачиПостановка задачи

Page 4: Appium для народа

4

Этапы решения задачиЭтапы решения задачи

11

устранение проблем

+

завершение интеграциив фреймворк

создание proof of concept

+

выявление проблем

выбор инструмента тестирования на мобильных платформах

22 33

Page 5: Appium для народа

5

Выбор инструментаВыбор инструмента

Требования:

open-source решение;

поддержка Java и WebDriver API;

поддержка ОС Android и iOS;

автоматизация приложений и браузеров;

работа на эмуляторах и физических устройствах;

активное развитие, наличие документации

Page 6: Appium для народа

6

Выбор инструментаВыбор инструмента

Page 7: Appium для народа

7

Выбор инструментаВыбор инструмента

iOS Android JavaWebDriver

APIЭмуля-торы

Устройства

Keep It Functional ✓ ✗ ✗

ObjectiveC✗ ✓ ✗

Frank ✓ ✗✗

Ruby + Cucumber

✗ ✓ ✗

Instruments(Apple) ✓ ✗ ✗

JavaScript✗ ✓ ✓

MonkeyTalk ✓ ✓✗

свой язык + JavaScript

✗ ✓ ✓

uiautomator(Google) ✗ ✓ ✓ ✗ ✓ ✓

Page 8: Appium для народа

8

Выбор инструментаВыбор инструмента

iOS Android JavaWebDriver

APIЭмуля-торы

Устройства

Robotium ✓ ✓ ✓ ✗ ✓ ✓

Calabash ✓ ✓ ~Cucumber; Ruby gems

✗ ✓ ✓

AndroidDriver(Selenium) ✗ ~

только браузер

✓ ✓ ✓ ✓

Selendroid ✗ ✓ ✓ ✓ ✓ ✓

ios–driver ✓ ✗ ✓ ✓ ✓ ~только

приложения

Appium ✓ ✓ ✓ ✓ ✓ ✓

Page 9: Appium для народа

9

Выбор инструментаВыбор инструмента

браузеры & приложения

браузеры &приложения

Page 10: Appium для народа

10

Appium: преимуществаAppium: преимущества

лёгкость внесения модификаций в серверную часть

работает без «агентов» в приложении

большой спектр поддерживаемых языков

распределённый запуск тестов (SeleniumGrid)

кросс-платформенность тестов

Page 11: Appium для народа

11

Appium: архитектураAppium: архитектура

Appium–сервер

Тестовыйсценарий

WebDriverJSONWire

Инструментавтоматизации

Прило-жение

uiautomator& selendroid

instruments

APIавтома-тизации

низко-уровневыекоманды

Page 12: Appium для народа

12

Proof of concept: первый запускProof of concept: первый запуск

Вручную: вернуть исходное состояние системы

Вручную: посмотреть Appium-логи

report

stories

mvn clean test

Вручную: запустить Appium

Вручную: Узнать UDID (для iOS-устройств)

Page 13: Appium для народа

13

Appium: общие проблемыAppium: общие проблемы

✗ в гибридных приложенияхнужно переключаться между native и webview частями

✗ между тест-кейсамиcookies браузера не очищаются

✗ Android: js-метод `click()` не работает

✗ iOS: не снимаются скриншоты

✗ нет отката к “чистому” состоянию

Page 14: Appium для народа

14

Appium: проблемы интеграцииAppium: проблемы интеграции

✗ Maven не запускает Appium автоматически

✗ разные “логи” у Maven и Appium

✗ iOS–устройства: нужно указывать UDID при запуске

✗ Android: Appium не запускает GenyMotion–эмулятор

Page 15: Appium для народа

15

Примеры решений:Примеры решений:запуск Appiumзапуск Appium

<profile> <id>mobile_unix</id> <activation> <property><name>mobile</name></property> <os><family>!windows</family></os> </activation> <build> <plugins> ...

<profile> <id>mobile_unix</id> <activation> <property><name>mobile</name></property> <os><family>!windows</family></os> </activation> <build> <plugins> ...

нет Appium-плагина для Maven✗

Page 16: Appium для народа

16

Примеры решений:Примеры решений:запуск Appiumзапуск Appium

<plugin> <groupId>org.codehaus.mojo</groupId>

<artifactId>exec-maven-plugin</artifactId> <version>1.2.1</version> <executions><execution>

<id>init</id><phase>validate</phase><goals><goal>exec</goal></goals><configuration> <executable>sh</executable> <environmentVariables>

<platform>${mobile}</platform> <isDevice>${device}</isDevice>

</environmentVariables> <commandlineArgs>-c 'source ./main.sh;

launch_appium'</commandlineArgs></configuration>

</execution></executions></plugin>

<plugin> <groupId>org.codehaus.mojo</groupId>

<artifactId>exec-maven-plugin</artifactId> <version>1.2.1</version> <executions><execution>

<id>init</id><phase>validate</phase><goals><goal>exec</goal></goals><configuration> <executable>sh</executable> <environmentVariables>

<platform>${mobile}</platform> <isDevice>${device}</isDevice>

</environmentVariables> <commandlineArgs>-c 'source ./main.sh;

launch_appium'</commandlineArgs></configuration>

</execution></executions></plugin>

Page 17: Appium для народа

17

Примеры решений:гибридные приложения

public class ShopScreen extends CommonMobileScreen {

private ElementLocator txtScreenTitle = new ElementLocator("Screen Title", "NATIVE_APP", By.xpath("//*[@id='title']");

public ShopScreen(WebDriverProvider driverProvider) { super(driverProvider);

addElements(new MobileElement[]{txtScreenTitle, lnkShopCategory}); }

}

public class ShopScreen extends CommonMobileScreen {

private ElementLocator txtScreenTitle = new ElementLocator("Screen Title", "NATIVE_APP", By.xpath("//*[@id='title']");

public ShopScreen(WebDriverProvider driverProvider) { super(driverProvider);

addElements(new MobileElement[]{txtScreenTitle, lnkShopCategory}); }

}

private MobileElement txtScreenTitle = new MobileElement ("Screen Title | xpath=//*[@id='title'] | NATIVE_APP");

private MobileElement lnkShopCategory = new MobileElement ("Category link | xpath=//div[text()='%s'] | WEBVIEW");

Native и Webview элементы – в разных фреймах✗

Page 18: Appium для народа

18

Примеры решений:Примеры решений:гибридные приложениягибридные приложения

public class CommonMobileMethods extends WebDriverPage {

private List<ElementLocator> elements = new ArrayList<ElementLocator>();

public ElementLocator getElementLocatorByName(String name) { for (MobileElement element : getElements()) { if (element.getName().equals(name)) {

this.switchTo().window(element.getType()); return element; } }

fail("[ERROR] Element '" + name + "' is not defined on '"+ MobileScreens.getCurrentScreen() + '" screen.");

return; }}

public class CommonMobileMethods extends WebDriverPage {

private List<ElementLocator> elements = new ArrayList<ElementLocator>();

public ElementLocator getElementLocatorByName(String name) { for (MobileElement element : getElements()) { if (element.getName().equals(name)) {

this.switchTo().window(element.getType()); return element; } }

fail("[ERROR] Element '" + name + "' is not defined on '"+ MobileScreens.getCurrentScreen() + '" screen.");

return; }}

this.switchTo().window(element.getType());

public MobileElement getElementLocatorByName(String name) {

Page 19: Appium для народа

19

Примеры решений:Примеры решений:замена javascript `click()` на Androidзамена javascript `click()` на Android

public class CommonMethods extends WebDriverPage {

public void jsClickElementByLoc(By loc) { if (isLocatorPresentOnPage(loc)) { WebElement element = findElement(loc); if (isBrowser("android")) { new TouchActions(getdriver()).singleTap(element).perform(); } else { JavascriptExecutor js = (JavascriptExecutor) getDriver(); js.executeScript("arguments[0].click()", element); } }}

public class CommonMethods extends WebDriverPage {

public void jsClickElementByLoc(By loc) { if (isLocatorPresentOnPage(loc)) { WebElement element = findElement(loc); if (isBrowser("android")) { new TouchActions(getdriver()).singleTap(element).perform(); } else { JavascriptExecutor js = (JavascriptExecutor) getDriver(); js.executeScript("arguments[0].click()", element); } }}

if (isBrowser("android")) { new TouchActions(getdriver()).singleTap(element).perform();

Android: JavaScript-клик не работает✗

Page 20: Appium для народа

20

Примеры решений:Примеры решений:снимки экрана на iOSснимки экрана на iOS

static class ScreenshootingRemoteWebDriver extends RemoteWebDriver implements TakesScreenshot { private static final String IOS_SCREENSHOT_CMD = "mobile :getScreenshot”; public <X> X getScreenshotAs(OutputType<X> target)

throws WebDriverException { String base64 = ""; if (isBrowser("ios")) { base64 = execute(IOS_SCREENSHOT_CMD).getValue().toString(); } else { base64 = execute(DriverCommand.SCREENSHOT).getValue().toString(); } return target.convertFromBase64Png(base64);}

static class ScreenshootingRemoteWebDriver extends RemoteWebDriver implements TakesScreenshot { private static final String IOS_SCREENSHOT_CMD = "mobile :getScreenshot”; public <X> X getScreenshotAs(OutputType<X> target)

throws WebDriverException { String base64 = ""; if (isBrowser("ios")) { base64 = execute(IOS_SCREENSHOT_CMD).getValue().toString(); } else { base64 = execute(DriverCommand.SCREENSHOT).getValue().toString(); } return target.convertFromBase64Png(base64);}

if (isBrowser("ios")) { base64 = execute(IOS_SCREENSHOT_CMD).getValue().toString(); } else { base64 = execute(DriverCommand.SCREENSHOT).getValue().toString(); }

private static final String IOS_SCREENSHOT_CMD = "mobile :getScreenshot”;

iOS: стандартная функция не снимает скриншоты✗

Page 21: Appium для народа

21

Примеры решений:Примеры решений:удаление приложенийудаление приложений

public class BaseStoriesRunner extends JUnitStories { protected static final String PROPERTY_MOBILE = "mobile”; @Test public void run() throws Throwable { try { beforeRun(); getEmbedder().runStoriesAsPaths(storyPath); } finally { afterRun(); } } protected void afterRun() { if (StringUtils.isNotBlank(System.getProperty(PROPERTY_MOBILE))) { uninstallMobileApps(); } } }

public class BaseStoriesRunner extends JUnitStories { protected static final String PROPERTY_MOBILE = "mobile”; @Test public void run() throws Throwable { try { beforeRun(); getEmbedder().runStoriesAsPaths(storyPath); } finally { afterRun(); } } protected void afterRun() { if (StringUtils.isNotBlank(System.getProperty(PROPERTY_MOBILE))) { uninstallMobileApps(); } } }

protected void afterRun() { if (StringUtils.isNotBlank(System.getProperty(PROPERTY_MOBILE))) { uninstallMobileApps(); } }

afterRun();

Нет отката к исходному состоянию✗

Page 22: Appium для народа

22

Примеры решений:Примеры решений:удаление приложенийудаление приложений

public class MobileUtils {

public static void uninstallMobileApps() { executeShCommand(UNINSTALL_APPS_COMMAND); }

private static void RunCommand(String command) { try { String line; Process p = new ProcessBuilder(command).start(); BufferedReader input = new BufferedReader(

new InputStreamReader(p.getInputStream())); while ((line = input.readLine()) != null) { System.out.println(line); } input.close(); } catch (Exception err) { err.printStackTrace(); } }}

public class MobileUtils {

public static void uninstallMobileApps() { executeShCommand(UNINSTALL_APPS_COMMAND); }

private static void RunCommand(String command) { try { String line; Process p = new ProcessBuilder(command).start(); BufferedReader input = new BufferedReader(

new InputStreamReader(p.getInputStream())); while ((line = input.readLine()) != null) { System.out.println(line); } input.close(); } catch (Exception err) { err.printStackTrace(); } }}

public static void uninstallMobileApps() { if isBrowser("ios") { RunCommand(IOS_UNINSTALL_APPS); }

if isBrowser("android") { RunCommand(IOS_UNINSTALL_APPS_COMMAND); } }

Process p = new ProcessBuilder(command).start();

Page 23: Appium для народа

23

Примеры решений:Примеры решений:удаление приложенийудаление приложений

public class MobileUtils { private final String SEPARATOR = "; "; private final String PROPERTY_ANDROID_APP_PACKAGE = "android.appPackage"; private final String PROPERTY_IOS_APP_BUNDLE = "ios.appBundle"; private final String ANDROID_SELENDROID_PACKAGE = "io.selendroid"; private final String ANDROID_BROWSER_PACKAGE = "io.selendroid.androiddriver"; private final String ANDROID_UNLOCK_PACKAGE = "io.appium.unlock"; private final String IOS_SAFARILAUNCHER_BUNDLE = "com.bytearc.SafariLauncher";

private final String UNINSTALL_APPS_COMMAND = ADB_PATH +" uninstall "+ System.getProperty(ANDROID_APP_PACKAGE) + QUIET + ADB_PATH +" uninstall "+ ANDROID_BROWSER_PACKAGE + QUIET + ADB_PATH +" uninstall "+ ANDROID_SELENDROID_PACKAGE + QUIET + ADB_PATH +" uninstall "+ ANDROID_UNLOCK_PACKAGE + QUIET + FRUITSTRAP_PATH +" uninstall --bundle "+ System.getProperty(IOS_APP_BUNDLE) + QUIET +

}

public class MobileUtils { private final String SEPARATOR = "; "; private final String PROPERTY_ANDROID_APP_PACKAGE = "android.appPackage"; private final String PROPERTY_IOS_APP_BUNDLE = "ios.appBundle"; private final String ANDROID_SELENDROID_PACKAGE = "io.selendroid"; private final String ANDROID_BROWSER_PACKAGE = "io.selendroid.androiddriver"; private final String ANDROID_UNLOCK_PACKAGE = "io.appium.unlock"; private final String IOS_SAFARILAUNCHER_BUNDLE = "com.bytearc.SafariLauncher";

private final String UNINSTALL_APPS_COMMAND = ADB_PATH +" uninstall "+ System.getProperty(ANDROID_APP_PACKAGE) + QUIET + ADB_PATH +" uninstall "+ ANDROID_BROWSER_PACKAGE + QUIET + ADB_PATH +" uninstall "+ ANDROID_SELENDROID_PACKAGE + QUIET + ADB_PATH +" uninstall "+ ANDROID_UNLOCK_PACKAGE + QUIET + FRUITSTRAP_PATH +" uninstall --bundle "+ System.getProperty(IOS_APP_BUNDLE) + QUIET +

}

private final String ANDROID_UNINSTALL_APPS_COMMAND = "adb uninstall " + System.getProperty(ANDROID_APP_PACKAGE) + SEPARATOR + "adb uninstall " + ANDROID_BROWSER_PACKAGE + SEPARATOR + "adb uninstall " + ANDROID_SELENDROID_PACKAGE + SEPARATOR + "adb uninstall " + ANDROID_UNLOCK_PACKAGE;

private final String IOS_UNINSTALL_APPS_COMMAND = "fruitstrap uninstall --bundle " + IOS_SAFARILAUNCHER_BUNDLE + SEPARATOR + "fruitstrap uninstall --bundle " + System.getProperty(IOS_APP_BUNDLE);

Page 24: Appium для народа

24

Примеры решений:Примеры решений:вывод Appium-ошибоквывод Appium-ошибок

public class CustomPerStoryWebDriverSteps extends PerStoryWebDriverSteps {

public static final String PROPERTY_MOBILE = "mobile";

@AfterScenario(uponOutcome = AfterScenario.Outcome.FAILURE) public void afterScenarioFailure(UUIDExceptionWrapper uuidWrappedFailure){ if (uuidWrappedFailure instanceof PendingStepFound) { return; } if (StringUtils.isNotBlank(System.getProperty(PROPERTY_MOBILE))) { MobileUtils.outputAppiumErrors(); } }}

public class CustomPerStoryWebDriverSteps extends PerStoryWebDriverSteps {

public static final String PROPERTY_MOBILE = "mobile";

@AfterScenario(uponOutcome = AfterScenario.Outcome.FAILURE) public void afterScenarioFailure(UUIDExceptionWrapper uuidWrappedFailure){ if (uuidWrappedFailure instanceof PendingStepFound) { return; } if (StringUtils.isNotBlank(System.getProperty(PROPERTY_MOBILE))) { MobileUtils.outputAppiumErrors(); } }}

if (StringUtils.isNotBlank(System.getProperty(PROPERTY_MOBILE))) { MobileUtils.outputAppiumErrors(); }

@AfterScenario(uponOutcome = AfterScenario.Outcome.FAILURE)

Appium-ошибки не показываются в логе Maven’а✗

Page 25: Appium для народа

25

Примеры решений:Примеры решений:вывод Appium-ошибоквывод Appium-ошибок

public class MobileUtils {

private final String APPIUM_LOG_PATH = "./target/mobile/appium.log"; private final String APPIUM_FULL_LOG_PATH = "./target/mobile/appium_full.log";

private static final String OUTPUT_APPIUM_ERRORS = "errors=$(grep 'error: \\|Error :\\|STDERR' "+ APPIUM_LOG_PATH +");" + "[ -n $errors ] && echo $errors | sed s/^/'[ERROR] appium.log: '/g\n" + " && cat ”+ APPIUM_LOG_PATH +" >>"+ APPIUM_FULL_LOG_PATH + " && echo '' >”+ APPIUM_LOG_PATH;

public static void outputAppiumErrors() { executeShCommand(OUTPUT_APPIUM_ERRORS); }}

public class MobileUtils {

private final String APPIUM_LOG_PATH = "./target/mobile/appium.log"; private final String APPIUM_FULL_LOG_PATH = "./target/mobile/appium_full.log";

private static final String OUTPUT_APPIUM_ERRORS = "errors=$(grep 'error: \\|Error :\\|STDERR' "+ APPIUM_LOG_PATH +");" + "[ -n $errors ] && echo $errors | sed s/^/'[ERROR] appium.log: '/g\n" + " && cat ”+ APPIUM_LOG_PATH +" >>"+ APPIUM_FULL_LOG_PATH + " && echo '' >”+ APPIUM_LOG_PATH;

public static void outputAppiumErrors() { executeShCommand(OUTPUT_APPIUM_ERRORS); }}

private static final String OUTPUT_APPIUM_ERRORS = "errors=$(grep 'error: \\|Error :\\|STDERR' " + APPIUM_LOG_PATH + ");" + "[ -n $errors ] && echo $errors | sed s/^/'[ERROR] appium.log: '/g\n" + " && cat " + APPIUM_LOG_PATH + " >>" + APPIUM_FULL_LOG_PATH + " && echo '' >" + APPIUM_LOG_PATH;

public static void outputAppiumErrors() { executeShCommand(OUTPUT_APPIUM_ERRORS); }

Page 26: Appium для народа

26

откат к «чистому» cостоянию

при ошибке: Appium-лог + скриншот✗

Итоговый фреймворк:Итоговый фреймворк:схема работысхема работы

очистка cookies

запуск тестов

запуск Appium сервера [и эмулятора]

сборка тестов

Page 27: Appium для народа

27

mobile = (?: android_browser | ios_safari | android_app | ios_app )

$ cat mobile.properties

ios.app = TestedApp.ipaios.simulatorType = iPhone (3.5 inch)ios.version = 7.0

android.app = TestedApp.apkandroid.waitActivity = com.app.MainActivityandroid.vmType = GenyMotionandroid.vmName = Nexus One - 4.2.2 - API 17 - 480x800

mobile = (?: android_browser | ios_safari | android_app | ios_app )

$ cat mobile.properties

ios.app = TestedApp.ipaios.simulatorType = iPhone (3.5 inch)ios.version = 7.0

android.app = TestedApp.apkandroid.waitActivity = com.app.MainActivityandroid.vmType = GenyMotionandroid.vmName = Nexus One - 4.2.2 - API 17 - 480x800

Итоговый фреймворк:Итоговый фреймворк:параметры запускапараметры запуска

$ mvn clean test -Dmobile=android_app -Ddevice=false –Dsuite=MobileTestSuite

$ mvn clean test -Dmobile=android_app -Ddevice=false –Dsuite=MobileTestSuite

$ cat mobile.properties

Page 28: Appium для народа

28

ЗаключениеЗаключение

запуск существующих веб-тестовв мобильных браузерах

разработка новых кросс-платформенных тестовдля мобильных сайтов и приложений

11

использование принятых на проекте практикв мобильном тестировании

22

33

Возможности полученного решения:

Page 29: Appium для народа

29

Перспективы развитияПерспективы развития

расширение корпоративной инфраструктуры SeleniumGrid нодами для мобильного тестирования

реализация решения в виде Maven-плагинадля интеграции в другие проекты

добавление возможности мобильного тестированияна ОС Windows Phone и Windows 8

увеличение количества мобильных браузеров, поддерживаемых фреймворком

Page 30: Appium для народа

Scalable eCommerce Platform SolutionsScalable eCommerce Platform Solutions

[email protected]

[email protected]