自動化之旅--Appium

2017-02-17 | Mio4kon | 自動化測試

概述

爲了不每次上線前重複的人工迴歸測試,保證每次上線的版本不會引發核心業務的不穩定,因此急需自動化測試來保證業務的穩定性.通過調研我嘗試使用Appium進行自動化測試,緣由是功能強大,跨平臺並且社區也很活躍.html

主流框架對比

Appium優勢

  • 開源
  • 跨架構:Native App、Hybird App、Web App
  • 跨設備:Android、iOS、Firefox OS
  • 不依賴源碼
  • 使用任何 WebDriver 兼容的語言來編寫測試用例。好比 Java, Objective-C, JavaScript with Node.js (in both callback and yield-based flavours), PHP, Python, Ruby, C#, Clojure, 或者 Perl.
  • 不須要從新編譯APP

若是有不清楚WebDriver的小夥伴立刻在Appium架構介紹中會有說明.java

Appium理念

  1. 你無需爲了自動化,而從新編譯或者修改你的應用。
  2. 你沒必要侷限於某種語言或者框架來寫和運行測試腳本。
  3. 一個移動自動化的框架不該該在接口上重複造輪子。(移動自動化的接口應該統一)
  4. 不管是精神上,仍是名義上,都必須開源。

Appium架構

iOS: 蘋果的UIAutomation
Android 4.2+: Google的UiAutomator
Android 2.3+: Google's Instrumentation. (由單獨的項目Selendroid提供支持 )android

Appium 1.6版本以上增長了UiAutomator2git

爲了知足上面跨平臺,把這些三方框架封裝成一套API —— WebDriver Api(客戶端到服務端的協議)github

事實上 WebDriver 已經成爲 web 瀏覽器自動化的標準,也成了 W3C 的標準 —— W3C Working Draft,因此Appium在原有基礎上擴充了移動自動化相關的API.web

投資 WebDriver 意味着你能夠押寶在一個已經成爲標準的獨立,自由和開放的協議。你不會被任何專利限制。npm

核心架構: Appium使用C/S架構,運行時候Service端會監聽Client端發送的命令,接着在移動設備上執行這些命令,而後將執行結果放在 HTTP 響應中返還給客戶端.瀏覽器

基於這架構能夠作什麼?bash

  • 能夠用任何實現了該客戶端的語言來寫測試代碼
  • 能夠把服務端放在不一樣的機器上
  • 能夠只寫測試代碼,而後利用相似 saucelabs 雲服務來解釋命令.

下圖解釋了雲服務的具體做用:服務器

Appium 使用

服務端

  • 安裝Appium服務器

    npm install -g appium
      npm install -g appium-doctor
      appium-doctor
    複製代碼

其中appium-doctor用來檢查電腦是否缺乏相關依賴.當全部都是對勾表示Appium環境配置完畢,以下:

  • 開啓appium服務器:

    appium --address 127.0.0.1 --port 4723 --log "/Users/mio4kon/Desktop/
      appium.log" --log-timestamp --local-timezone --session-override
    複製代碼

客戶端

再次強調 Appium 支持各類語言,這裏我選擇JAVA.若是以爲JAVA語法不夠簡潔或者不熟悉,可使用你所熟悉的語言.

建立 MAVEN/Gradle 工程:

建立工程,並加入下面依賴:

<dependency>
            <groupId>io.appium</groupId>
            <artifactId>java-client</artifactId>
            <version>5.0.0-BETA2</version>
            <exclusions>
                <exclusion>
                    <groupId>org.seleniumhq.selenium</groupId>
                    <artifactId>selenium-java</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>3.0.1</version>
        </dependency>
複製代碼

這樣Appium 客戶端的依賴就引入成功了.

Capabiltiy配置:

每一個Test的基類中定義setUp方法,並設置Capabiltiy之前其餘的初始化操做:

DesiredCapabilities capabilities = new DesiredCapabilities ();
capabilities.setCapability (MobileCapabilityType.DEVICE_NAME, deviceName);
capabilities.setCapability (MobileCapabilityType.PLATFORM_NAME, platformName);
capabilities.setCapability (MobileCapabilityType.PLATFORM_VERSION, platformVersion);
capabilities.setCapability (MobileCapabilityType.APP, apkPath);
capabilities.setCapability (AndroidMobileCapabilityType.APP_PACKAGE, appPackage);
capabilities.setCapability (AndroidMobileCapabilityType.APP_ACTIVITY, appActivity);
capabilities.setCapability (MobileCapabilityType.AUTOMATION_NAME, AutomationName.ANDROID_UIAUTOMATOR2);

複製代碼

這裏我使用的是 TestNG 中的Parameters註釋來配置參數.

TestNG 又是什麼鬼? 簡單來講TestNG是java的一個測試框架,相似JUnit但功能更增強大,使用也方便.

TestNG

利用TestNg的一些註釋,作準備化和收尾操做.

上面的setUp方法我就使用了BeforeClassParameters這兩種註釋.

@BeforeClass
    @Parameters({"driverName", "url", "deviceName", "platformName", "platformVersion", "apkPath", "appPackage", "appActivity"})
    public void setUp(String driverName, String url, String deviceName, String platformName, String platformVersion, String apkPath, String appPackage, String appActivity) throws Exception {
        log.i (TAG, "BeforeClass");
        driver = setRemoteDriver (driverName, url, deviceName, platformName, platformVersion, apkPath, appPackage, appActivity);
        actions = ElementActions.getInstance ().init (driver);
        assertActions = actions.getAssertActions ();
        Screenshot.getInstance ().init (driver);
        prepare ();
    }
複製代碼

一樣在在AfterClass後,進行了driver的退出.

正常狀況下咱們寫測試用例是下面這種狀況:

一個TestClass中包含多個TestMethod.若是每一個TestMethod都相互獨立,須要從新運行APP顯然很是耗時,因此這裏我將每個TestClass相互獨立,而其中的TestMethod又相互依賴,執行順序經過TestNG的XML來控制.以下:

<test name="XX測試">
        <classes>
            <class name="XXTest">
                <methods>
                    <include name="testAAA"/>
                    <include name="testBBB"/>
                    <include name="testCCC"/>
                </methods>
            </class>
        </classes>
    </test>
複製代碼

可是有些狀況若是須要TestMethod也相互獨立的話,能夠利用AfterMethod註釋.

@AfterMethod
    public void afterMethod() {
        driver.resetApp ();
    }
複製代碼

編寫用例

查找元素

定位方式

查找元素能夠經過不少方法(能夠經過UIAutomatorViewer獲取頁面的id,name等信息):

  • id
  • name
  • className
  • xpath
  • uiautomator

高級定位

  • xpath查找登陸按鈕:

    by.xpath ("//button[@name='login']")
    複製代碼

xpath實例教程

  • uiautomator的API滾動查找:
String rule = "new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textContains(\"" + locator.value + "\"))"
	WebElement cl = driver.findElementByAndroidUIAutomator(rule));
複製代碼

其實就是用若是用uiautomator來寫:

new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textContains(value));
複製代碼

uiautomator API

### 元素管理
複製代碼

爲了讓測試用例可以方便複用,簡化寫用例的時間,必要的封裝是不可少的. 利用 Page Object 模式將頁面上的元素進行封裝.這樣全部 Test 只須要簡單的控制頁面元素便可.

  • 可使用yaml文件進行管理頁面元素:

而後在BasePage中將yaml解析封裝成Locator對象.並保存到集合.
這樣全部的PageObject均可以經過下面方法來定位元素.

protected Locator getLocator(String locatorName) {
        checkNotNull (locatorName);
        Page page = getPage ();
        List<Locator> locators = page.locators;
        for(Locator locator : locators) {
            if (locatorName.equals (locator.name)) {
                return locator;
            }
        }
        return null;
    }
    
public Locator 請輸入手機號 = getLocator ("請輸入手機號");
public Locator 請輸入驗證碼 = getLocator ("請輸入驗證碼");
public Locator 發送驗證碼 = getLocator ("發送驗證碼");

複製代碼

TODO: 用工具生成對應page類
之所這麼作是能夠在用的時候直接經過IDE提示有哪些控件,可是寫上述代碼缺都是無聊的操做,全部我用了 freemarker 自動解析yaml來生成對應Page類,以下:

Map<String, Object> input = new HashMap<String, Object> ();
        input.put ("pageName", page.pageName);
        List<LocatorObject> genLocators = new ArrayList<> ();
        List<Locator> locators = page.locators;
        for(int i = 0; i < locators.size (); i++) {
            System.out.println ("生成Locator:" + locators.get (i).name);
            genLocators.add (new LocatorObject (locators.get (i).name, locators.get (i).name));
        }
        input.put ("locators", genLocators);

        Template template = cfg.getTemplate ("page-temp.ftl");

        Writer fileWriter = new FileWriter (new File (path, page.pageName + ".java"));
        try {
            template.process (input, fileWriter);
        } finally {
            fileWriter.close ();
        }
複製代碼

若是不會用 freemarker的能夠搜索下相關資料.

元素交互

將交互事件,封裝在ElementActions中.使用起來很是簡單:

actions.text (loginPage.請輸入手機號, phone);
actions.click (loginPage.發送驗證碼);
actions.text (loginPage.請輸入驗證碼, pwd);
actions.click (loginPage.登陸); 
複製代碼

經常使用的交互事件:單擊,連擊,上下左右滑動,後退,輸入文本等等.

上面元素定位其實只是封裝Locator,並無真正的查找元素,查找元素其實是在ElementActions中處理的.因爲應用常常處理網絡請求,控件有時候須要過段時間纔可見,因此在查找控件時候須要給予必定的時間.

WebElement element;
        try {
            element = (new WebDriverWait (mDriver, locator.timeOutInSeconds)).until (
                    new ExpectedCondition<WebElement> () {

                        @Override
                        public WebElement apply(WebDriver driver) {
                            List<WebElement> elements = getElement (locator);
                            if (elements.size () != 0) {
                                return elements.get (0);
                            }
                            return null;
                        }
                    });

        } catch (NoSuchElementException | TimeoutException e) {
            log.e (TAG, "超時[%4$d秒],找不到元素:[%1$s] , [By.%2$s : %3$s]", locator.name, locator.type, locator.value, locator.timeOutInSeconds);
            throw e;
        }
複製代碼

斷言

一樣爲了使用方便講經常使用的斷言封裝,如Toast驗證等.

public void validatesToast(final String msg) {
        checkNotNull (msg);
        final WebDriverWait wait = new WebDriverWait (mDriver, 10);
        assertNotNull (wait.until (ExpectedConditions
                                           .presenceOfElementLocated (By.xpath (String.format ("//*[@text=\'%s\']", msg)))));
    }

複製代碼

測試報告

測試報告能夠直觀的展現測試的成功率,截圖等信息.這裏選擇extentreports作爲測試報告的框架.

最終效果:

踩坑

  • findElementByName無效.

Searching by name was deprecated over a year ago and removed from 1.5. In general, searching by accessibility id is better for a variety of reasons.

如上findElementByName這個方法從Appium 1.5以後刪除了,可是API不經能找到而且也沒提示過期.這不坑爹嘛.後來使用下面的代碼才解決用name查找元素的方法.

String query = "new UiSelector().textContains" + "(\"" + locator.value + "\")";
 webElements = mDriver.findElementsByAndroidUIAutomator (query);
複製代碼
  • 聽說Appium 1.6.3能夠查找 Toast 的信息了.而後屁顛屁顛的跑去試了下網上的例子發現很差使啊.一度覺得是Client版本的問題.搞了半天才發現須要加下面的代碼:
capabilities.setCapability (MobileCapabilityType.AUTOMATION_NAME, AutomationName.ANDROID_UIAUTOMATOR2);
複製代碼

(感受如今的Appium文檔不全並且有點亂

  • 無心中發現測試的時候高德地圖彈Toast報錯.而後直接編譯安裝卻不保存.猜想是否是在安裝過程當中Appium改了啥.看了下Service日誌,居然在安裝的時候從新簽名...
App not signed with debug cert.
2017-02-13 18:17:19:848 - info: [debug] [ADB] Resigning apk.
2017-02-13 18:17:23:938 - info: [debug] [ADB] Zip-aligning 'app-debug.apk'
2017-02-13 18:17:23:958 - info: [ADB] Checking whether zipalign is present
2017-02-13 18:17:23:964 - info: [ADB] Using zipalign from /Users/mio4kon/Library/Android/sdk/build-tools/25.0.2/zipalign
2017-02-13 18:17:23:968 - info: [debug] [ADB] Zip-aligning apk.
2017-02-13 18:17:24:104 - info: [AndroidDriver] Remote apk path is /data/local/tmp/463eb03788048b4a1dacfe28545ee76e.apk
複製代碼

解決方法:

capabilities.setCapability (AndroidMobileCapabilityType.NO_SIGN, true);

相關文章
相關標籤/搜索