自動化之旅--Appium

餓了麼物流技術團隊發表於2018-03-16
2017-02-17 | Mio4kon | 自動化測試

概述

為了避免每次上線前重複的人工迴歸測試,保證每次上線的版本不會引起核心業務的不穩定,所以急需自動化測試來保證業務的穩定性.經過調研我嘗試使用Appium進行自動化測試,原因是功能強大,跨平臺而且社群也很活躍.

主流框架對比

自動化之旅--Appium

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架構介紹中會有說明.

Appium理念

  1. 你無需為了自動化,而重新編譯或者修改你的應用。
  2. 你不必侷限於某種語言或者框架來寫和執行測試指令碼。
  3. 一個移動自動化的框架不應該在介面上重複造輪子。(移動自動化的介面應該統一)
  4. 無論是精神上,還是名義上,都必須開源。

Appium架構

自動化之旅--Appium

iOS: 蘋果的UIAutomation
Android 4.2+: Google的UiAutomator
Android 2.3+: Google's Instrumentation. (由單獨的專案Selendroid提供支援 )

Appium 1.6版本以上增加了UiAutomator2

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

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

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

核心架構: Appium使用C/S架構,執行時候Service端會監聽Client端傳送的命令,接著在移動裝置上執行這些命令,然後將執行結果放在 HTTP 響應中返還給客戶端.

基於這架構可以做什麼?

  • 可以用任何實現了該客戶端的語言來寫測試程式碼
  • 可以把服務端放在不同的機器上
  • 可以只寫測試程式碼,然後利用類似 saucelabs 雲服務來解釋命令.

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

自動化之旅--Appium

Appium 使用

服務端

  • 安裝Appium伺服器

      npm install -g appium
      npm install -g appium-doctor
      appium-doctor
    複製程式碼

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

自動化之旅--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註釋來配置引數.

自動化之旅--Appium

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

TestNG

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

自動化之旅--Appium

上面的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檔案進行管理頁面元素:

自動化之旅--Appium

然後在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做為測試報告的框架.

最終效果:

自動化之旅--Appium

踩坑

  • 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);

相關文章