appium для народа
DESCRIPTION
Доклад Дмитрия Черемушкина на SQA Days-15. 18-19 апреля, 2014, Москва. www.sqadays.comTRANSCRIPT
Черемушкин Дмитрий
инженерпо автоматизации тестирования ПО
Scalable eCommerce Platform SolutionsОбо мнеОбо мне
6+ лет в тестировании ПО
ручное | автоматизированное
настольное | веб | мобильное
автоматизация рутинных
действий в QA-процессах,
интеграция инструментов
Черемушкин Дмитрий
1
2
Постановка задачиПостановка задачи
Имеется: фреймворк,основанный на стеке технологий:
+ автоматизированные тесты для eCommerce веб-сайта~ 2000 тесткейсов • Firefox, Chrome, IE • локальный и удаленный запуск
3
1 Запустить имеющиеся тесты(в Android Browser и Mobile Safari)
Разработать новые тесты(мобильная версия сайта + Android-приложение)
2
Необходимо:
на мобильных ОС Android и iOS
Постановка задачиПостановка задачи
4
Этапы решения задачиЭтапы решения задачи
11
устранение проблем
+
завершение интеграциив фреймворк
создание proof of concept
+
выявление проблем
выбор инструмента тестирования на мобильных платформах
22 33
5
Выбор инструментаВыбор инструмента
Требования:
open-source решение;
поддержка Java и WebDriver API;
поддержка ОС Android и iOS;
автоматизация приложений и браузеров;
работа на эмуляторах и физических устройствах;
активное развитие, наличие документации
6
Выбор инструментаВыбор инструмента
7
Выбор инструментаВыбор инструмента
iOS Android JavaWebDriver
APIЭмуля-торы
Устройства
Keep It Functional ✓ ✗ ✗
ObjectiveC✗ ✓ ✗
Frank ✓ ✗✗
Ruby + Cucumber
✗ ✓ ✗
Instruments(Apple) ✓ ✗ ✗
JavaScript✗ ✓ ✓
MonkeyTalk ✓ ✓✗
свой язык + JavaScript
✗ ✓ ✓
uiautomator(Google) ✗ ✓ ✓ ✗ ✓ ✓
8
Выбор инструментаВыбор инструмента
iOS Android JavaWebDriver
APIЭмуля-торы
Устройства
Robotium ✓ ✓ ✓ ✗ ✓ ✓
Calabash ✓ ✓ ~Cucumber; Ruby gems
✗ ✓ ✓
AndroidDriver(Selenium) ✗ ~
только браузер
✓ ✓ ✓ ✓
Selendroid ✗ ✓ ✓ ✓ ✓ ✓
ios–driver ✓ ✗ ✓ ✓ ✓ ~только
приложения
Appium ✓ ✓ ✓ ✓ ✓ ✓
9
Выбор инструментаВыбор инструмента
браузеры & приложения
браузеры &приложения
10
Appium: преимуществаAppium: преимущества
лёгкость внесения модификаций в серверную часть
работает без «агентов» в приложении
большой спектр поддерживаемых языков
распределённый запуск тестов (SeleniumGrid)
кросс-платформенность тестов
11
Appium: архитектураAppium: архитектура
Appium–сервер
Тестовыйсценарий
WebDriverJSONWire
Инструментавтоматизации
Прило-жение
uiautomator& selendroid
instruments
APIавтома-тизации
низко-уровневыекоманды
12
Proof of concept: первый запускProof of concept: первый запуск
Вручную: вернуть исходное состояние системы
Вручную: посмотреть Appium-логи
report
stories
mvn clean test
Вручную: запустить Appium
Вручную: Узнать UDID (для iOS-устройств)
13
Appium: общие проблемыAppium: общие проблемы
✗ в гибридных приложенияхнужно переключаться между native и webview частями
✗ между тест-кейсамиcookies браузера не очищаются
✗ Android: js-метод `click()` не работает
✗ iOS: не снимаются скриншоты
✗ нет отката к “чистому” состоянию
14
Appium: проблемы интеграцииAppium: проблемы интеграции
✗ Maven не запускает Appium автоматически
✗ разные “логи” у Maven и Appium
✗ iOS–устройства: нужно указывать UDID при запуске
✗ Android: Appium не запускает GenyMotion–эмулятор
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
Примеры решений:Примеры решений:запуск 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
Примеры решений:гибридные приложения
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
Примеры решений:Примеры решений:гибридные приложениягибридные приложения
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
Примеры решений:Примеры решений:замена 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
Примеры решений:Примеры решений:снимки экрана на 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
Примеры решений:Примеры решений:удаление приложенийудаление приложений
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
Примеры решений:Примеры решений:удаление приложенийудаление приложений
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
Примеры решений:Примеры решений:удаление приложенийудаление приложений
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
Примеры решений:Примеры решений:вывод 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
Примеры решений:Примеры решений:вывод 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); }
26
откат к «чистому» cостоянию
при ошибке: Appium-лог + скриншот✗
Итоговый фреймворк:Итоговый фреймворк:схема работысхема работы
очистка cookies
запуск тестов
запуск 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
28
ЗаключениеЗаключение
запуск существующих веб-тестовв мобильных браузерах
разработка новых кросс-платформенных тестовдля мобильных сайтов и приложений
11
использование принятых на проекте практикв мобильном тестировании
22
33
Возможности полученного решения:
29
Перспективы развитияПерспективы развития
расширение корпоративной инфраструктуры SeleniumGrid нодами для мобильного тестирования
реализация решения в виде Maven-плагинадля интеграции в другие проекты
добавление возможности мобильного тестированияна ОС Windows Phone и Windows 8
увеличение количества мобильных браузеров, поддерживаемых фреймворком
Scalable eCommerce Platform SolutionsScalable eCommerce Platform Solutions