基于python+appium+yaml安卓UI自動(dòng)化測(cè)試分享

結(jié)構(gòu)介紹

之前分享過(guò)一篇安卓UI測(cè)試垃帅,但是沒有實(shí)現(xiàn)數(shù)據(jù)與代碼分離铝阐,后期維護(hù)成本較高宏粤,所以最近抽空優(yōu)化了一下。
不想看文章得可以直接去Github骂因,歡迎拍磚
大致結(jié)構(gòu)如下:

結(jié)構(gòu).png

  • testyaml管理用例炎咖,實(shí)現(xiàn)數(shù)據(jù)與代碼分離,一個(gè)模塊一個(gè)文件夾

  • public 存放公共文件,如讀取配置文件乘盼、啟動(dòng)appium服務(wù)升熊、讀取Yaml文件、定義日志格式等

  • page 存放最小測(cè)試用例集绸栅,一個(gè)模塊一個(gè)文件夾

  • results 存放測(cè)試報(bào)告及失敗截圖


    report.png
  • logs 存放日志


    logs.png

    logdetail.png
  • testcase 存放測(cè)試用例
  • runtest.py 運(yùn)行所有測(cè)試用例

yaml格式介紹

首先看下yaml文件的格式级野,之前也寫過(guò)一點(diǎn)關(guān)于yaml語(yǔ)法學(xué)習(xí)的文章
testcase部分是重點(diǎn),其中:

  • element_info:定位元素信息

  • find_type:屬性粹胯,id蓖柔、xpath、text风纠、ids

  • operate_type: click况鸣、sendkeys、back竹观、swipe_up 為back就是返回镐捧,暫時(shí)就四種

    上面三個(gè)必填,operate_type必填!!!!!!

  • send_content:send_keys 時(shí)用到

  • index:ids時(shí)用到

  • times: 返回次數(shù)或者上滑次數(shù)

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

代碼部分

公共部分

個(gè)人覺得核心的就是公共部分栈幸,相當(dāng)于建房子愤估,公共部分搞好了,后面僅僅是調(diào)用即可速址,建房子把架子搭好,后面就添磚加瓦吧由驹。

讀取配置文件readconfig.py
設(shè)置日志格式logs.py
獲取設(shè)備GetDevices.py
這幾個(gè)通用的就不做介紹了

  • 讀取yaml文件 GetYaml.py
    主要用來(lái)讀取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"未找到y(tǒng)aml文件")

    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']

  • 啟動(dòng)appium服務(wù) StartAppiumServer.py
    主要是啟動(dòng)appium并返回端口port,這個(gè)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):
        """
        啟動(dòng)appium
        p:appium port
        bp:bootstrap port
        :return: 返回appium端口參數(shù)
        """
        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: 啟動(dòng)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文件中并炮,可以直接通過(guò)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等一些基礎(chǔ)操作
#coding=utf-8
#author='Shichao-Dong'

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

'''
一些基礎(chǔ)操作:滑動(dòng)澜搅、截圖伍俘、點(diǎn)擊頁(yè)面元素等
'''

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):
        '''
        向上滑動(dòng)
        :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):
        '''
        判斷頁(yè)面是否存在某個(gè)元素
        :param name: text
        :return:
        '''
        findname = "http://*[@text='%s']"%(name)
        exsit = self.driver.find_element_by_xpath(findname)
        if exsit :
            return True
        else:
            return False

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

        findname = "http://*[@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):
        '''
        定位頁(yè)面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):
        '''
        定位頁(yè)面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):
        '''
        定位頁(yè)面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):
        '''
        返回至指定頁(yè)面
        :return:
        '''
        i=0
        while i<10:
            i=i+1
            try:
                findname = "http://*[@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 = "http://*[@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("http://*[@text='工作臺(tái)']")
                    self.driver.implicitly_wait(2)
                    break
                except:
                    os.popen("adb shell input keyevent 4")
  • Operate.py
    我認(rèn)為最關(guān)鍵的一步了,后面沒有page都是調(diào)用這個(gè)文件進(jìn)行測(cè)試勉躺,主要是根據(jù)讀取的yaml文件癌瘾,然后進(jìn)行if...else...判斷,根據(jù)對(duì)應(yīng)的operate_type分別進(jìn)行對(duì)應(yīng)的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信息并執(zhí)行
        element_info:定位元素信息
        find_type:屬性妨退,id、xpath、text咬荷、ids
        operate_type: click冠句、sendkeys、back幸乒、swipe_up 為back就是返回轩端,暫時(shí)就三種

        上面三個(gè)必填,operate_type必填!!!!!!

        send_content:send_keys 時(shí)用到
        index:ids時(shí)用到
        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):
        '''
        返回至工作臺(tái)
        :return:
        '''
        self.baseoperate.page('工作臺(tái)')

公共部分的代碼就介紹這么多逝变,在編寫這個(gè)框架的時(shí)候基茵,大部分精力都花在這部分,所以個(gè)人覺得還是值得好好研究的

Page部分

page部分是最小用例集壳影,一個(gè)模塊一個(gè)文件夾拱层,以客戶為例,
目前寫了兩個(gè)用例宴咧,一個(gè)新增根灯,一個(gè)排序,文件如下:


file.png

代碼如下掺栅,非常的簡(jiǎn)潔,

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()

運(yùn)行用例

這部分用了unittest,運(yùn)行所有測(cè)試用例和生成報(bào)告烙肺。
一個(gè)模塊一個(gè)用例,以客戶為例: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層將需要運(yùn)行的用例都import進(jìn)來(lái)氧卧,然后用unittest運(yùn)行即可桃笙。
如果想要運(yùn)行所有的測(cè)試用例,需要用到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()

這邊的思路差不多沙绝,也是先導(dǎo)入再裝入suite即可

總結(jié)

就目前而言搏明,暫時(shí)算是實(shí)現(xiàn)了數(shù)據(jù)與用例的分離,但是yaml的編寫要求較高闪檬,不能格式上出錯(cuò)星著。
同時(shí)也有一些其他可以優(yōu)化的地方,如:

  • 對(duì)彈窗的判斷
  • 斷開后重連機(jī)制
  • 失敗后重跑機(jī)制
    等等粗悯,后續(xù)可以根據(jù)需求進(jìn)行優(yōu)化
    最后再貼一下開源地址Github虚循,有興趣的小伙伴可以去看一下,歡迎拍磚
    備注:完成過(guò)程中參考了Louis-meauto 這兩個(gè)開源項(xiàng)目样傍,感謝:岬蕖!铭乾!
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末剪廉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子炕檩,更是在濱河造成了極大的恐慌斗蒋,老刑警劉巖捌斧,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異泉沾,居然都是意外死亡捞蚂,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門跷究,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)姓迅,“玉大人,你說(shuō)我怎么就攤上這事俊马《〈妫” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵柴我,是天一觀的道長(zhǎng)解寝。 經(jīng)常有香客問(wèn)我,道長(zhǎng)艘儒,這世上最難降的妖魔是什么聋伦? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮界睁,結(jié)果婚禮上觉增,老公的妹妹穿的比我還像新娘。我一直安慰自己翻斟,他們只是感情好逾礁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杨赤,像睡著了一般敞斋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上疾牲,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音衙解,去河邊找鬼阳柔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蚓峦,可吹牛的內(nèi)容都是我干的舌剂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼暑椰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼霍转!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起一汽,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤避消,失蹤者是張志新(化名)和其女友劉穎低滩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岩喷,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡恕沫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纱意。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片婶溯。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖偷霉,靈堂內(nèi)的尸體忽然破棺而出迄委,到底是詐尸還是另有隱情,我是刑警寧澤类少,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布叙身,位于F島的核電站,受9級(jí)特大地震影響瞒滴,放射性物質(zhì)發(fā)生泄漏曲梗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一妓忍、第九天 我趴在偏房一處隱蔽的房頂上張望虏两。 院中可真熱鬧,春花似錦世剖、人聲如沸定罢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)祖凫。三九已至,卻和暖如春酬凳,著一層夾襖步出監(jiān)牢的瞬間惠况,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工宁仔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留稠屠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓翎苫,卻偏偏與公主長(zhǎng)得像权埠,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子煎谍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容