Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Appium для народа

5,982 views

Published on

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

Published in: Education

Appium для народа

  1. 1. Черемушкин Дмитрий инженер по автоматизации тестирования ПО Scalable eCommerce Platform Solutions Обо мнеОбо мне 6+ лет в тестировании ПО ручное | автоматизированное настольное | веб | мобильное автоматизация рутинных действий в QA-процессах, интеграция инструментов Черемушкин Дмитрий 1
  2. 2. 2 Постановка задачиПостановка задачи Имеется: фреймворк, основанный на стеке технологий: + автоматизированные тесты для eCommerce веб-сайта ~ 2000 тесткейсов • Firefox, Chrome, IE • локальный и удаленный запуск
  3. 3. 3 1 Запустить имеющиеся тесты (в Android Browser и Mobile Safari) Разработать новые тесты (мобильная версия сайта + Android-приложение) 2 Необходимо: на мобильных ОС Android и iOS Постановка задачиПостановка задачи
  4. 4. 4 Этапы решения задачиЭтапы решения задачи 11 устранение проблем + завершение интеграции в фреймворк создание proof of concept + выявление проблем выбор инструмента тестирования на мобильных платформах 22 33
  5. 5. 5 Выбор инструментаВыбор инструмента Требования: open-source решение; поддержка Java и WebDriver API; поддержка ОС Android и iOS; автоматизация приложений и браузеров; работа на эмуляторах и физических устройствах; активное развитие, наличие документации
  6. 6. 6 Выбор инструментаВыбор инструмента
  7. 7. 7 Выбор инструментаВыбор инструмента iOS Android Java WebDriver API Эмуля- торы Устройства Keep It Functional ✓ ✗ ✗ ObjectiveC ✗ ✓ ✗ Frank ✓ ✗ ✗ Ruby + Cucumber ✗ ✓ ✗ Instruments (Apple) ✓ ✗ ✗ JavaScript ✗ ✓ ✓ MonkeyTalk ✓ ✓ ✗ свой язык + JavaScript ✗ ✓ ✓ uiautomator (Google) ✗ ✓ ✓ ✗ ✓ ✓
  8. 8. 8 Выбор инструментаВыбор инструмента iOS Android Java WebDriver API Эмуля- торы Устройства Robotium ✓ ✓ ✓ ✗ ✓ ✓ Calabash ✓ ✓ ~ Cucumber; Ruby gems ✗ ✓ ✓ AndroidDriver (Selenium) ✗ ~ только браузер ✓ ✓ ✓ ✓ Selendroid ✗ ✓ ✓ ✓ ✓ ✓ ios–driver ✓ ✗ ✓ ✓ ✓ ~ только приложения Appium ✓ ✓ ✓ ✓ ✓ ✓
  9. 9. 9 Выбор инструментаВыбор инструмента браузеры & приложения браузеры & приложения
  10. 10. 10 Appium: преимуществаAppium: преимущества лёгкость внесения модификаций в серверную часть работает без «агентов» в приложении большой спектр поддерживаемых языков распределённый запуск тестов (SeleniumGrid) кросс-платформенность тестов
  11. 11. 11 Appium: архитектураAppium: архитектура Appium– сервер Тестовый сценарий WebDriver JSON Wire Инструмент автоматизации Прило- жение uiautomator & selendroid instruments API автома- тизации низко- уровневые команды
  12. 12. 12 Proof of concept: первый запускProof of concept: первый запуск Вручную: вернуть исходное состояние системы Вручную: посмотреть Appium-логи report stories mvn clean test Вручную: запустить Appium Вручную: Узнать UDID (для iOS-устройств)
  13. 13. 13 Appium: общие проблемыAppium: общие проблемы ✗ в гибридных приложениях нужно переключаться между native и webview частями ✗ между тест-кейсами cookies браузера не очищаются ✗ Android: js-метод `click()` не работает ✗ iOS: не снимаются скриншоты ✗ нет отката к “чистому” состоянию
  14. 14. 14 Appium: проблемы интеграцииAppium: проблемы интеграции ✗ Maven не запускает Appium автоматически ✗ разные “логи” у Maven и Appium ✗ iOS–устройства: нужно указывать UDID при запуске ✗ Android: Appium не запускает GenyMotion–эмулятор
  15. 15. 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✗
  16. 16. 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>
  17. 17. 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 элементы – в разных фреймах✗
  18. 18. 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) {
  19. 19. 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-клик не работает✗
  20. 20. 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: стандартная функция не снимает скриншоты✗
  21. 21. 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(); Нет отката к исходному состоянию✗
  22. 22. 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();
  23. 23. 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);
  24. 24. 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’а✗
  25. 25. 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: '/gn" + " && 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: '/gn" + " && 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: '/gn" + " && cat " + APPIUM_LOG_PATH + " >>" + APPIUM_FULL_LOG_PATH + " && echo '' >" + APPIUM_LOG_PATH; public static void outputAppiumErrors() { executeShCommand(OUTPUT_APPIUM_ERRORS); }
  26. 26. 26 откат к «чистому» cостоянию при ошибке: Appium-лог + скриншот✗ Итоговый фреймворк:Итоговый фреймворк: схема работысхема работы очистка cookies запуск тестов запуск Appium сервера [и эмулятора] сборка тестов
  27. 27. 27 mobile = (?: android_browser | ios_safari | android_app | ios_app ) $ cat mobile.properties ios.app = TestedApp.ipa ios.simulatorType = iPhone (3.5 inch) ios.version = 7.0 android.app = TestedApp.apk android.waitActivity = com.app.MainActivity android.vmType = GenyMotion android.vmName = Nexus One - 4.2.2 - API 17 - 480x800 mobile = (?: android_browser | ios_safari | android_app | ios_app ) $ cat mobile.properties ios.app = TestedApp.ipa ios.simulatorType = iPhone (3.5 inch) ios.version = 7.0 android.app = TestedApp.apk android.waitActivity = com.app.MainActivity android.vmType = GenyMotion android.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
  28. 28. 28 ЗаключениеЗаключение запуск существующих веб-тестов в мобильных браузерах разработка новых кросс-платформенных тестов для мобильных сайтов и приложений 11 использование принятых на проекте практик в мобильном тестировании 22 33 Возможности полученного решения:
  29. 29. 29 Перспективы развитияПерспективы развития расширение корпоративной инфраструктуры SeleniumGrid нодами для мобильного тестирования реализация решения в виде Maven-плагина для интеграции в другие проекты добавление возможности мобильного тестирования на ОС Windows Phone и Windows 8 увеличение количества мобильных браузеров, поддерживаемых фреймворком
  30. 30. Scalable eCommerce Platform SolutionsScalable eCommerce Platform Solutions Dmitry.Cheremushkin@gmail.com DCheremushkin@griddynamics.com

×