基於python+appium+yaml安卓UI自動化測試分享

zouhui1003it發表於2018-02-09

結構介紹

之前分享過一篇安卓UI測試,但是沒有實現資料與程式碼分離,後期維護成本較高,所以最近抽空優化了一下。
不想看文章得可以直接去Github,歡迎拍磚
大致結構如下:

結構.png
  • testyaml管理用例,實現資料與程式碼分離,一個模組一個資料夾

  • public 存放公共檔案,如讀取配置檔案、啟動appium服務、讀取Yaml檔案、定義日誌格式等

  • page 存放最小測試用例集,一個模組一個資料夾

  • results 存放測試報告及失敗截圖

    report.png
  • logs 存放日誌

    logs.png
    logdetail.png
  • testcase 存放測試用例
  • runtest.py 執行所有測試用例

yaml格式介紹

首先看下yaml檔案的格式,之前也寫過一點關於yaml語法學習的文章
testcase部分是重點,其中:

  • element_info:定位元素資訊

  • find_type:屬性,id、xpath、text、ids

  • operate_type: click、sendkeys、back、swipe_up 為back就是返回,暫時就四種

    上面三個必填,operate_type必填!!!!!!

  • send_content:send_keys 時用到

  • index:ids時用到

  • times: 返回次數或者上滑次數

testinfo:
    - id: cm001
      title: 新增終端門店
      execute: 1
testcase:
    -
      element_info: 客戶
      find_type: text
      operate_type: click
    -
      element_info: com.fiberhome.waiqin365.client:id/cm_topbar_tv_right
      find_type: id
      operate_type: click
    -
      element_info: com.fiberhome.waiqin365.client:id/custview_id_singletv_inputtext
      find_type: ids
      operate_type: send_keys
      send_content: auto0205
      index: 0
    -
      element_info:
      find_type:
      operate_type: swipe_up
      times: 1
    -
      element_info: 提交
      find_type: text
      operate_type: click
    -
      element_info:
      find_type:
      operate_type: back
      times: 1

程式碼部分

公共部分

個人覺得核心的就是公共部分,相當於建房子,公共部分搞好了,後面僅僅是呼叫即可,建房子把架子搭好,後面就添磚加瓦吧。

讀取配置檔案readconfig.py
設定日誌格式logs.py
獲取裝置GetDevices.py
這幾個通用的就不做介紹了

  • 讀取yaml檔案 GetYaml.py
    主要用來讀取yaml檔案
#coding=utf-8
#author=`Shichao-Dong`

import sys
reload(sys)
sys.setdefaultencoding(`utf8`)
import yaml
import codecs

class getyaml:
    def __init__(self,path):
        self.path = path

    def getYaml(self):
        ```
        讀取yaml檔案
        :param path: 檔案路徑
        :return:
        ```
        try:
            f = open(self.path)
            data =yaml.load(f)
            f.close()
            return data
        except Exception:
            print(u"未找到yaml檔案")

    def alldata(self):
        data =self.getYaml()
        return data

    def caselen(self):
        data = self.alldata()
        length = len(data[`testcase`])
        return length

    def get_elementinfo(self,i):
        data = self.alldata()
        # print data[`testcase`][i][`element_info`]
        return data[`testcase`][i][`element_info`]

    def get_findtype(self,i):
        data = self.alldata()
        # print data[`testcase`][i][`find_type`]
        return data[`testcase`][i][`find_type`]

    def get_operate_type(self,i):
        data = self.alldata()
        # print data[`testcase`][i][`operate_type`]
        return data[`testcase`][i][`operate_type`]

    def get_index(self,i):
        data = self.alldata()
        if self.get_findtype(i)==`ids`:
                    return data[`testcase`][i][`index`]
        else:
            pass

    def get_send_content(self,i):
        data = self.alldata()
        # print data[`testcase`][i][`send_content`]
        if self.get_operate_type(i) == `send_keys`:
            return data[`testcase`][i][`send_content`]
        else:
            pass

    def get_backtimes(self,i):
        data = self.alldata()
        if self.get_operate_type(i)==`back` or self.get_operate_type(i)==`swipe_up`:
                    return data[`testcase`][i][`times`]
        else:
            pass

    def get_title(self):
        data = self.alldata()
        # print data[`testinfo`][0][`title`]
        return  data[`testinfo`][0][`title`]

  • 啟動appium服務 StartAppiumServer.py
    主要是啟動appium並返回埠port,這個port在下面的driver中需要
#coding=utf-8
#author=`Shichao-Dong`

from logs import log
import random,time
import platform
import os
from GetDevices import devices

log = log()
dev = devices().get_deviceName()

class Sp:
    def __init__(self, device):
        self.device = device

    def __start_driver(self, aport, bpport):
        """
        :return:
        """
        if platform.system() == `Windows`:
            import subprocess
            subprocess.Popen("appium -p %s -bp %s -U %s" %
                             (aport, bpport, self.device), shell=True)

    def start_appium(self):
        """
        啟動appium
        p:appium port
        bp:bootstrap port
        :return: 返回appium埠引數
        """
        aport = random.randint(4700, 4900)
        bpport = random.randint(4700, 4900)
        self.__start_driver(aport, bpport)

        log.info(
            `start appium :p %s bp %s device:%s` %
            (aport, bpport, self.device))
        time.sleep(10)
        return aport

    def main(self):
        """
        :return: 啟動appium
        """
        return self.start_appium()

    def stop_appium(self):
        ```
        停止appium
        :return:
        ```
        if platform.system() == `Windows`:
            os.popen("taskkill /f /im node.exe")

if __name__ == `__main__`:
    s = Sp(dev)
    s.main()
  • 獲取driver GetDriver.py
    platformName、deviceName、appPackage、appActivity這些解除安裝配置檔案config.ini檔案中,可以直接通過readconfig.py檔案讀取獲得。
    appium_port有StartAppiumServer.py檔案返回
s = Sp(deviceName)
appium_port = s.main()

def mydriver():
    desired_caps = {
                `platformName`:platformName,`deviceName`:deviceName, `platformVersion`:platformVersion,
                `appPackage`:appPackage,`appActivity`:appActivity,
                `unicodeKeyboard`:True,`resetKeyboard`:True,`noReset`:True
                }
    try:
        driver = webdriver.Remote(`http://127.0.0.1:%s/wd/hub`%appium_port,desired_caps)
        time.sleep(4)
        log.info(`獲取driver成功`)
        return driver
    except WebDriverException:
        print `No driver`

if __name__ == "__main__":
    mydriver()
  • 重新封裝find等命令,BaseOperate.py
    裡面主要是一些上滑、返回、find等一些基礎操作
#coding=utf-8
#author=`Shichao-Dong`

from selenium.webdriver.support.ui import WebDriverWait
from logs import log
import os
import time

```
一些基礎操作:滑動、截圖、點選頁面元素等
```

class BaseOperate:
    def __init__(self,driver):
        self.driver = driver

    def back(self):
        ```
        返回鍵
        :return:
        ```
        os.popen("adb shell input keyevent 4")

    def get_window_size(self):
        ```
        獲取螢幕大小
        :return: windowsize
        ```
        global windowSize
        windowSize = self.driver.get_window_size()
        return windowSize

    def swipe_up(self):
        ```
        向上滑動
        :return:
        ```
        windowsSize = self.get_window_size()
        width = windowsSize.get("width")
        height = windowsSize.get("height")
        self.driver.swipe(width/2, height*3/4, width/2, height/4, 1000)

    def screenshot(self):
        now=time.strftime("%y%m%d-%H-%M-%S")
        PATH = lambda p: os.path.abspath(
            os.path.join(os.path.dirname(__file__), p)
        )
        screenshoot_path = PATH(`../results/screenshoot/`)
        self.driver.get_screenshot_as_file(screenshoot_path+now+`.png`)

    def find_id(self,id):
        ```
        尋找元素
        :return:
        ```
        exsit = self.driver.find_element_by_id(id)
        if exsit :
            return True
        else:
            return False

    def find_name(self,name):
        ```
        判斷頁面是否存在某個元素
        :param name: text
        :return:
        ```
        findname = "//*[@text=`%s`]"%(name)
        exsit = self.driver.find_element_by_xpath(findname)
        if exsit :
            return True
        else:
            return False

    def get_name(self,name):
        ```
        定位頁面text元素
        :param name:
        :return:
        ```
        # element = driver.find_element_by_name(name)
        # return element

        findname = "//*[@text=`%s`]"%(name)
        try:
            element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_xpath(findname))
            # element = self.driver.find_element_by_xpath(findname)
            self.driver.implicitly_wait(2)
            return element
        except:
            self.screenshot()
            log.error(`未定位到元素:`+`%s`)%(name)

    def get_id(self,id):
        ```
        定位頁面resouce-id元素
        :param id:
        :return:
        ```
        try:
            element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_id(id))
            # element = self.driver.find_element_by_id(id)
            self.driver.implicitly_wait(2)
            return element
        except:
            self.screenshot()
            log.error(`未定位到元素:`+`%s`)%(id)

    def get_xpath(self,xpath):
        ```
        定位頁面xpath元素
        :param id:
        :return:
        ```
        try:
            element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_xpath(xpath))
            # element = self.driver.find_element_by_xpath(xpath)
            self.driver.implicitly_wait(2)
            return element
        except:
            self.screenshot()
            log.error(`未定位到元素:`+`%s`)%(xpath)

    def get_ids(self,id):
        ```
        定位頁面resouce-id元素組
        :param id:
        :return:列表
        ```
        try:
            # elements = self.driver.find_elements_by_id(id)
            elements = WebDriverWait(self.driver, 10).until(lambda x: x.find_elements_by_id(id))
            self.driver.implicitly_wait(2)
            return elements
        except:
            self.screenshot()
            log.error(`未定位到元素:`+`%s`)%(id)

    def page(self,name):
        ```
        返回至指定頁面
        :return:
        ```
        i=0
        while i<10:
            i=i+1
            try:
                findname = "//*[@text=`%s`]"%(name)
                self.driver.find_element_by_xpath(findname)
                self.driver.implicitly_wait(2)
                break
            except :
                os.popen("adb shell input keyevent 4")
                try:
                    findname = "//*[@text=`確定`]"
                    self.driver.find_element_by_xpath(findname).click()
                    self.driver.implicitly_wait(2)
                except:
                    os.popen("adb shell input keyevent 4")
                try:
                    self.driver.find_element_by_xpath("//*[@text=`工作臺`]")
                    self.driver.implicitly_wait(2)
                    break
                except:
                    os.popen("adb shell input keyevent 4")
  • Operate.py
    我認為最關鍵的一步了,後面沒有page都是呼叫這個檔案進行測試,主要是根據讀取的yaml檔案,然後進行if…else…判斷,根據對應的operate_type分別進行對應的click、sendkeys等操作
#coding=utf-8
#author=`Shichao-Dong`

from GetYaml import getyaml
from BaseOperate import BaseOperate

class Operate:
    def __init__(self,path,driver):
        self.path = path
        self.driver = driver
        self.yaml = getyaml(self.path)
        self.baseoperate=BaseOperate(driver)

    def check_operate_type(self):
        ```
        讀取yaml資訊並執行
        element_info:定位元素資訊
        find_type:屬性,id、xpath、text、ids
        operate_type: click、sendkeys、back、swipe_up 為back就是返回,暫時就三種

        上面三個必填,operate_type必填!!!!!!

        send_content:send_keys 時用到
        index:ids時用到
        times:
        :return:
        ```

        for i in range(self.yaml.caselen()):
            if self.yaml.get_operate_type(i) == `click`:
                if self.yaml.get_findtype(i) == `text`:
                    self.baseoperate.get_name(self.yaml.get_elementinfo(i)).click()
                elif self.yaml.get_findtype(i) == `id`:
                    self.baseoperate.get_id(self.yaml.get_elementinfo(i)).click()
                elif self.yaml.get_findtype(i) == `xpath`:
                    self.baseoperate.get_xpath(self.yaml.get_elementinfo(i)).click()
                elif self.yaml.get_findtype(i) == `ids`:
                    self.baseoperate.get_ids(self.yaml.get_elementinfo(i))[self.yaml.get_index(i)].click()

            elif self.yaml.get_operate_type(i) == `send_keys`:
                if self.yaml.get_findtype(i) == `text`:
                    self.baseoperate.get_name(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
                elif self.yaml.get_findtype(i) == `id`:
                    self.baseoperate.get_id(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
                elif self.yaml.get_findtype(i) == `xpath`:
                    self.baseoperate.get_xpath(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
                elif self.yaml.get_findtype(i) == `ids`:
                    self.baseoperate.get_ids(self.yaml.get_elementinfo(i))[self.yaml.get_index(i)].send_keys(self.yaml.get_send_content(i))

            elif self.yaml.get_operate_type(i) == `back`:
                for n in range(self.yaml.get_backtimes(i)):
                    self.baseoperate.back()

            elif self.yaml.get_operate_type(i) == `swipe_up`:
                for n in range(self.yaml.get_backtimes(i)):
                    self.baseoperate.swipe_up()

    def back_home(self):
        ```
        返回至工作臺
        :return:
        ```
        self.baseoperate.page(`工作臺`)

公共部分的程式碼就介紹這麼多,在編寫這個框架的時候,大部分精力都花在這部分,所以個人覺得還是值得好好研究的

Page部分

page部分是最小用例集,一個模組一個資料夾,以客戶為例,
目前寫了兩個用例,一個新增,一個排序,檔案如下:

file.png

程式碼如下,非常的簡潔,

import sys
reload(sys)
sys.setdefaultencoding(`utf8`)
import codecs,os
from public.Operate import Operate
from public.GetYaml import getyaml

PATH = lambda p: os.path.abspath(
    os.path.join(os.path.dirname(__file__), p)
)
yamlpath = PATH("../../testyaml/cm/cm-001addcm.yaml")

class AddcmPage:

    def __init__(self,driver):
        self.path = yamlpath
        self.driver = driver
        self.operate = Operate(self.path,self.driver)

    def operateap(self):
        self.operate.check_operate_type()

    def home(self):
        self.operate.back_home()

執行用例

這部分用了unittest,執行所有測試用例和生成報告。
一個模組一個用例,以客戶為例:CmTest.py

from page.cm.CmAddcmPage import AddcmPage
from page.cm.CmSortcmPage import SortcmPage


from public.GetDriver import mydriver
driver = mydriver()

import unittest,time
class Cm(unittest.TestCase):

    def test_001addcm(self):
        ```
        新增客戶
        :return:
        ```
        add = AddcmPage(driver)
        add.operateap()
        add.home()
    def test_002sortcm(self):
        ```
        客戶排序
        :return:
        ```
        sort = SortcmPage(driver)
        sort.sortlist()
        sort.home()

    def test_999close(self):
        driver.quit()
        time.sleep(10)

if __name__ == "__main__":
    unittest.main()

首先從page層將需要執行的用例都import進來,然後用unittest執行即可。
如果想要執行所有的測試用例,需要用到runtest.py

import time,os
import unittest
import HTMLTestRunner
from testcase.CmTest import Cm


def testsuit():
    suite = unittest.TestSuite()
    suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(Cm),




])

    # runner = unittest.TextTestRunner(verbosity=2)
    # runner.run(suite)

    now=time.strftime("%y-%m-%d-%H-%M-%S")
    PATH = lambda p: os.path.abspath(
        os.path.join(os.path.dirname(__file__), p)
    )
    dirpath = PATH("./results/waiqin365-")

    filename=dirpath + now +`result.html`
    fp=open(filename,`wb`)
    runner=HTMLTestRunner.HTMLTestRunner(stream=fp,title=`waiqin365 6.0.6beta test result`,description=u`result:`)

    runner.run(suite)
    fp.close()

if __name__ =="__main__":
    testsuit()

這邊的思路差不多,也是先匯入再裝入suite即可

總結

就目前而言,暫時算是實現了資料與用例的分離,但是yaml的編寫要求較高,不能格式上出錯。
同時也有一些其他可以優化的地方,如:

  • 對彈窗的判斷
  • 斷開後重連機制
  • 失敗後重跑機制
    等等,後續可以根據需求進行優化
    最後再貼一下開源地址Github,有興趣的小夥伴可以去看一下,歡迎拍磚
    備註:完成過程中參考了Louis-meauto 這兩個開源專案,感謝!!!

作者:邁阿密小白
連結:https://www.jianshu.com/p/00aff8435a92
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。


相關文章