Appium Appium+python 框架 (二)

woyebuzhidaowoshishei · 2015年10月23日 · 最后由 vitcui 回复于 2019年01月15日 · 6057 次阅读
本帖已被设为精华帖!

前言

之前已经发过一个,许多人给提了很宝贵的意见,根据大家的意见和自己的一点思考,对原来的框架进行了一点修改,这里给大家分享一下,还是请各位看完后多多提意见。

结构

大体的结构没有太大的变化,这里附上原帖地址 https://testerhome.com/topics/3460
修改的地方有以下几个:
1.在 testSet 下增加了一个 bsns 文件夹,里面有 bsnsCommon.py;element.xml;TestCase.xls3 个文件夹
2.common 里面增加 AppiumServer.py;将 myPhone.py 变为 init.py
3.增加了 autoRun.bat 和 install.bat

修改处

1.讲 AppiumServer 从 run.py 中抽离出来,封装成了 AppiumServer.py
2.弃用自己写的 Log 方法,使用了 python 自带的 logging
3.讲 element 的路径配置进了 element.xml 中
4.实现了测试数据参数化
5.做成了 bat 文件调用 run.py
6.修改了注释风格

下面仔细说一下

AppiumServer

这个借鉴了 cosyman 以前发过的帖子,原帖地址 https://testerhome.com/topics/1864
总结而言就是原先用线程做的现在改成了进程。代码如下:

class AppiumServer:

    def __init__(self):
        global appiumPath, baseUrl
        appiumPath = readConfigLocal.getConfigValue("appiumPath")
        baseUrl = readConfigLocal.getConfigValue("baseUrl")

    def startServer(self):
        """start the appium server
        :return:
        """
        cmd = self.getCmd()
        t1 = runServer(cmd)
        p = Process(target=t1.start())
        p.start()

    def stopServer(self):
        """stop the appium server
        :return:
        """
        #kill myServer
        os.system('taskkill /f /im node.exe')

    def reStartServer(self):
        """reStart the appium server
        :arg:
        :return:
        """
        self.stopServer()
        self.startServer()

    def isRunnnig(self):
        """Determine whether server is running
        :return:True or False
        """
        response = None
        url = baseUrl+"/status"
        try:
            response = urllib.request.urlopen(url, timeout=5)

            if str(response.getcode()).startswith("2"):
                return True
            else:
                return False
        except URLError:
            return False
        finally:
            if response:
                response.close()

    def getCmd(self):
        """get the cmd of start appium server
        :return:cmd
        """
        rootDirectory = appiumPath[:2]
        startCMD = "node node_modules\\appium\\bin\\appium.js"

        cmd =rootDirectory+"&"+"cd "+appiumPath+"&"+startCMD

        return cmd

import threading


class runServer(threading.Thread):

    def __init__(self, cmd):
        threading.Thread.__init__(self)
        self.cmd = cmd

    def run(self):
        os.system(self.cmd)

if __name__ == "__main__":
    oo = AppiumServer()
    oo.startServer()

Log

原先是自己写的 log 方法,现在是使用了 python 自带的 logging,部分代码如下:

self.logger = logging.getLogger()
self.logger.setLevel(logging.INFO)

#create handler,write log
fh = logging.FileHandler(os.path.join(logPath, "outPut.log" ))
#Define the output format of formatter handler
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)

self.logger.addHandler(fh)

这里我并没有使用 logging 的配置文件,而是直接写在的代码中

element.xml

这里也是借鉴了 xushizhao 的帖子,原帖地址如下 https://testerhome.com/topics/2937
为什么要这样做我就不多说,原帖里都说了,代码如下:
1.element.xml
与原帖不同的是我在 element 标签外面添加了一个 activity 标签,这样就可以不必要同个标签配置多次了。

<activity name = "GuideActivity"><!--activity名称-->

       <element name="welcome"> <!-- 元素对象 -->
            <name>Welcome</name><!-- 元素名称 -->
            <type>RelativeLayout</type><!-- 元素类型 input/button -->
            <pathtype>ID</pathtype><!-- 获取元素的模式 ID/XPATH/CLASSNAME/NAME-->
            <pathvalue>ag_ll_dotlayout</pathvalue><!-- 元素定位路径 -->
        </element>

</activity >

2.调用方法

activity = {}
def setXml():
    """
    get the xml file's value
    :use:
    a = getXml(path)

    print(a.get(".module.GuideActivity").get("skip").get("type"))
    :param: xmlPath
    :return:activity
    """
    if len(activity) == 0:
        xmlPath = os.path.join(readConfig.prjDir, "testSet\\bsns", "element.xml")
        # open the xml file
        per = ET.parse(xmlPath)
        allElement = per.findall('activity')

        for firstElement in allElement:
            activityName = firstElement.get("name")

            element = {}
            for secondElement in firstElement.getchildren():
                elementName = secondElement.get("name")

                elementChild = {}
                for thirdElement in secondElement.getchildren():

                    elementChild[thirdElement.tag] = thirdElement.text

                element[elementName] = elementChild
            activity[activityName] = element

def getElDict(activityName, elementName):
    """
    According to the activityName and elementName get element
    :param activityNmae:
    :param elementName:
    :return:
    """
    setXml()
    elementDict = activity.get(activityName).get(elementName)
    return elementDict

class element:

    def __init__(self, activutyName, elementName):
        global driver
        driver = myDriver.GetDriver()
        self.activutyNmae = activutyNmae
        self.elementName = elementName
        elementDict = getElDict(self.activutyNmae, self.elementName)
        self.pathtype = elementDict.get("pathtype")
        self.pathvalue = elementDict.get("pathvalue")

    def isExist(self):
        """
        To determine whether an element is exits
        :return: TRUE or FALSE
        """
        try:
            if self.pathtype == "ID":
                driver.find_element_by_id(self.pathvalue)
            if self.pathtype == "CLASSNAME":
                driver.find_element_by_class_name(self.pathvalue)
            if self.pathtype == "XPATH":
                driver.find_element_by_xpath(self.pathvalue)
            if self.pathtype == "NAME":
                driver.find_element_by_name(self.pathvalue)
        except NoSuchElementException:
            return False
        return True

    def doesExist(self):
        """
        To determine whether an element is exits
        :return:
        """
        i = 1
        while not self.isExist():
            sleep(1)
            i = i+1
            if i >= 10:
                return False
        else:
            return True

    def get(self):
        """
        get one element
        :return:
        """
        if self.doesExist():
            if self.pathtype == "ID":
                element = driver.find_element_by_id(self.pathvalue)
                return element
            if self.pathtype == "CLASSNAME":
                element = driver.find_element_by_class_name(self.pathvalue)
                return element
            if self.pathtype == "XPATH":
                element = driver.find_element_by_xpath(self.pathvalue)
                return element
            if self.pathtype == "NAME":
                element = driver.find_element_by_name(self.pathvalue)
                return element
        else:
            return None

    def gets(self, index):
        """
        get one element in elementList
        :return:
        """
        if self.doesExist():
            if self.pathtype == "ID":
                elements = driver.find_elements_by_id(self.pathvalue)
                return elements[index]
            if self.pathtype == "CLASSNAME":
                elements = driver.find_elements_by_class_name(self.pathvalue)
                return elements[index]
            if self.pathtype == "XPATH":
                elements = driver.find_elements_by_xpath(self.pathvalue)
                return elements[index]
            if self.pathtype == "NAME":
                elements = driver.find_elements_by_name(self.pathvalue)
                return elements[index]
            return None
        else:
            return None

    def click(self):
        """
        click element
        :return:
        """
        try:
            el = self.get()
            el.click()
        except AttributeError:
            raise

    def clicks(self, index):
        """
        click element
        :return:
        """
        try:
            el = self.gets(index)
            el.click()
        except AttributeError:
            raise

    def sendKey(self,values):
        """
        input the key
        :return:
        """
        try:
            el = self.get()
            el.clear()
            el.send_keys(values)
        except AttributeError:
            raise

    def sendKeys(self, index, values):
        """
        input the key
        :return:
        """
        try:
            el = self.gets(index)
            el.clear()
            el.send_keys(values)
        except AttributeError:
            raise

    def getAttribute(self, attribute):
        """
        get the element attribute
        :param attribute:
        :return:value
        """
        el = self.get()
        value = el.get_attribute(attribute)
        return value

根据 anctivityName 和 elementName 获取 element,使用的时候可以这样用:element(anctivityName ,elementName).click()

测试数据参数化

这里我是使用了 ParamUnittest,官网地址如下大家可以咨询下载:https://pypi.python.org/pypi/ParamUnittest#downloads
将测试数据配置到 excel 里面,然后读取。代码如下:

读取 excel

import xlrd
cls = []
def getXLS(sheetName):
    """
    get the value in excel
    :param sheetName
    :return:cls
    """

    if len(cls) == 0:
        xlsPath = os.path.join(readConfig.prjDir, "testSet\\bsns", "TestCase.xls")

        #read the excel
        data = xlrd.open_workbook(xlsPath)

        #get the sheet
        table = data.sheet_by_name(sheetName)

        nrows = table.nrows

        for i in range(nrows):

            if table.row_values(i)[0] != 'userName':
                cls.append(table.row_values(i))
    return cls

ParamUnittest 的使用

loginCls = getLoginCls()

@user1trized(
        *loginCls
    )

class TestBar(paramunittest.ParametrizedTestCase):


    def setParameters(self, userName,password,result):
        self.userName = userName
        self.password = password
        self.result = result


    def runTest(self):
        print(self.userName, self.password, self.result)

ParamUnittest 的例官网里面有好多,大家可以自己去研究。

bat

本来是想在 bat 文件里面开启 server,识别安装软件,后来发现自己的能力有限,要考虑的东西有点多,后来放弃了,改在 py 文件里面完成,然后仅在 bat 文件里面调用。

ECHO START INSTALL
F:  
cd F:\testApp01  
start pythonw testSet\init.py
ECHO END INSTALL
PAUSE 

总结

1.生成的报告不太满意,目前还是自己实现的,不知道各位有什么好的推荐?
2.异常机制依旧没有完善的太好,继续努力。
3.论坛里面有太多好帖子了,感觉大神的分享。
4.希望大家在看完帖子后可以留下你的意见,感谢!!!

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 55 条回复 时间 点赞

之前学习的都是 Appium+RobotFrameword,实现起来很简单,所以没有深挖学习。看了这个感觉自己需要学习的东西真是太多了~~~感谢分享!!

2楼 已删除

#2 楼 @haiquan180 额,我实现了啊。。封装了 AppiumServer,在 run.py 里面调用的。

4楼 已删除

#4 楼 @haiquan180 我还是没有太明白你的意思,autoRun.bat 这个只是运行 run.py 而已,server 的启动和停止我是封装在 AppiumServer.py 里面,server 的启动时在 run.py 里面调用了 AppiumServer 的 startServer()而已。不知道你是不是这个意思?

#4 楼 @haiquan180 你的意思是融合到框架代码还是测试用例代码?
如果是框架代码,现在的实现就已经做到了。
如果是测试代码,这个不是太合理,毕竟测试代码不应该关心 appium server 状态。

那个 bat 文件只是入口,方便放在任意地方双击直接运行而已。它做的是启动整个测试,而不光是启动 appium server 。

7楼 已删除

学习了,能不能传到 github 上?这样方便大家学习

棒! 加油

请教个 appium 问题,事先已经网搜过,了解过定位方法
页面底部 “吃饭”“讨饭”“我的” 按钮,红框中三个属性都相
只有 text 属性不同,怎么定位?

#11 楼 @x746560359 可以通过 xpath,或者 class。通过 class 的话可以获取一个 element list,然后通过下标获取你想要的那个 element。

def gets(self, index):
  elements = driver.find_elements_by_id(self.pathvalue)
  return elements[index]

例如这样

生成的报告不太满意,目前还是自己实现的,不知道各位有什么好的推荐?

可以尝试一下HTMLTestRunner,如果不满意可以尝试改一下源码,上网找一个好一点的模版,很容易就做出比较好看的报告来。

#13 楼 @duyanguang 谢谢,我会去试试的。

activutyNmae 这个是故意这么打的吗。。。 处女座表示很难受

def isExist(self):
    """
    To determine whether an element is exits
    :return: TRUE or FALSE
    """
    try:
        if self.pathtype == "ID":
            driver.find_element_by_id(self.pathvalue)
        if self.pathtype == "CLASSNAME":
            driver.find_element_by_class_name(self.pathvalue)
        if self.pathtype == "XPATH":
            driver.find_element_by_xpath(self.pathvalue)
        if self.pathtype == "NAME":
            driver.find_element_by_name(self.pathvalue)
    except NoSuchElementException:
        return False
    return True

self.pathtype 这个是怎么获取的呢? 我看不大明白, 这个 isExist 怎么用,能讲解一下吗?

element(anctivityName ,elementName).isExist()

这样吗

#17 楼 @lylyliuyu 在 element 的init方法里面获取的

# 根据activutyNmae 和 elementName 获取指定 element
 elementDict = getElDict(self.activutyNmae, self.elementName)
# 获取element的 pathtype 和 pathvalue 值
 self.pathtype = elementDict.get("pathtype")
 self.pathvalue = elementDict.get("pathvalue")

然后调用的话 就 element(anctivityName ,elementName).isExist() 这样调用。

你好,class element 这个类调用驱动( myDriver.GetDriver())的能说详细点吗,我之前是带入驱动,那样会很麻烦,而且多一个参数,在用例那看着很不美观。

#19 楼 @wxhhxx123 element 类在 init 方法中获取了 driver,由于我这里的 driver 是单例函数,所以我直接就是获取了我在 run.py 中打开的 driver。不知道我有没有说明白?

#20 楼 @tongshanshanshan python 水平有限,单例百度了下还是不太明白,想问下,GetDriver() 这个返回的是什么?我自己写了个 driver 函数,调用时候又重连了次 app,导致 session 冲突。

#21 楼 @wxhhxx123 单例的意思就是在运行过程中,有且只有一个实例对象。GetDriver() 这个返回的就是 driver 实例。你写的 driver 函数,是重新又实例化了一个 driver 对象,所以会导致 session 冲突。

#22 楼 @tongshanshanshan 好的,我应该明白了,多谢了!这种文章太好了,appium 的能写的这么详细的确实很少,而且还是 python 的,得到了很多参考。

#22 楼 @tongshanshanshan
class allDriver():
instance=None
mutex=threading.Lock()
@staticmethod
def getDriver():
if(allDriver.instance==None):
allDriver.mutex.acquire()
if(allDriver.instance==None):
PrintInfo('初始化实例')
chromedriver = "C:\Program Files\Google\Chrome\Application\chromedriver.exe"
os.environ["webdriver.chrome.driver"] = chromedriver
allDriver.instance=webdriver.Chrome(chromedriver)
else:
PrintInfo('单例已经实例化 111')
allDriver.mutex.release()
else:
PrintInfo('单例已经实例化 222')
return allDriver.instance
我用 selenium 中写了个,返回实例的,效果达到了,不过返回是实例,在 element 类中无法匹配 webdriver 的函数(不知道这样说你能否理解),请指教。

#22 楼 @tongshanshanshan

class allDriver():
    instance=None
    mutex=threading.Lock()
    @staticmethod
    def getDriver():
        if(allDriver.instance==None):
            allDriver.mutex.acquire()
            if(allDriver.instance==None):
                PrintInfo('初始化实例')
                chromedriver = "C:\Program Files\Google\Chrome\Application\chromedriver.exe"
                os.environ["webdriver.chrome.driver"] = chromedriver
                allDriver.instance=webdriver.Chrome(chromedriver)
            else:
                PrintInfo('单例已经实例化111')
            allDriver.mutex.release()
        else:
            PrintInfo('单例已经实例化222')
        return allDriver.instance

不好意思,之前那个有点乱,重新编辑了下。

#24 楼 @wxhhxx123 你的意思是无法调用 webdriver 的函数?报什么错?

#26 楼 @tongshanshanshan
没有报错,就是如果返回 webdriver.Chrome(chromedriver) 可以如图匹配 webdriver 的函数,但返回实例时候就没有了,难道是编辑器的问题吗,我用的是 pycharm。

#27 楼 @wxhhxx123 返回实例应该也是可以调用的,你写上,运行一下,应该不回报错。

#12 楼 @tongshanshanshan
请问怎么取出某个元素点击?我取出后都不能调用 click 方法了,都是一些里列表调用方法了。
另外就是,xpath 的路径太长,怎么写简短有效?
如果第一次的路径被找出来了,第二次又多了相同的元素,这种情况如何处理?只能修改脚本吗?

30楼 已删除

#30 楼 @haiquan180 赞同,我最近也再看这个,但碰到很多问题

#30 楼 @haiquan180 testerhome 有官方的 qq 群啊

赞一个 (: element.xml 这个文件是手动维护的吗?

def is_exist(self):
"""
To determine whether an element is exits
:return: TRUE or FALSE
"""
try:
if self.path_type == "ID":
driver.find_element_by_id(self.path_value)
return True
if self.path_type == "CLASSNAME":
driver.find_element_by_class_name(self.path_value)
return True
if self.path_type == "XPATH":
driver.find_element_by_xpath(self.path_value)
return True
if self.path_type == "NAME":
driver.find_element_by_name(self.path_value)
return True
return False
except NoSuchElementException:
return False

driver.find_element_by_xx() 后,不判断下是否未 None,直接 return True 吗?

#34 楼 @cinderella 如果没有找到该 element,则会产生 NoSuchElementException 异常,所以如果为 none 就是反回 false。还有 element.xml 目前还是手动维护。

#35 楼 @tongshanshanshan

if not By.is_valid(by) or not isinstance(value, str):
raise InvalidSelectorException("Invalid locator values passed in")
看了下源码,未找到 element 的时候,产生的是 InvalidSelectorException,该处为什么是 NoSuchElementException 异常呢?

多谢 多谢

#36 楼 @cinderella InvalidSelectorException 这个异常捕获的是你传入的 by 和 value 是无效的参数时所产生的异常,并不是没有找到 element 时所报的异常。

不好意思啊,我水平有限,我想问下,为什么要使用 ini 文件呢,把那些东西比如 app 的那些参数 platformName,deviceName,appActivity 等等这些东西放在一个 py 文件里,然后在代码启动的时候先调用这个 py 文件不就行了吗,感觉和放在 ini 文件里没有什么区别啊。

tongshanshanshan ,您好,我在用您这个框架,from urllib.error import URLError 报错,提示没有 urllib.error,我注释掉相关 URLError 后,脚本一直在 is_running() 这循环,下面的第二个 print "+"*50 一直没打印,appium server 的 status 网址返回的是 info: [debug] Responding to client with success: {"status":0,"value":{"build":{"version":"1.4.8","revision":"c8179bd8352d3c62d98fa58b324491230df9342a"}}},不是 200,我用的是 macbook pro,麻烦指点一下,谢谢,我的 QQ3085109012

def run(self):
"""run test
:return:
"""
try:
suit = self.create_suite()
if suit is not None:
logger.info("Begin to start Appium Server")
self.myServer.start_server()
sleep(30)

while not self.myServer.is_runnnig():
sleep(3)
print "-"*50,self.myServer.is_runnnig()
else:
print "+"*50,self.myServer.is_runnnig()
logger.info("End to start Appium Server")
logger.info("Open Driver")

#39 楼 @app_test 我用的 python3.4,你用的应该是 python2.7,2.7 里面没有 urllib.error。

#40 楼 @tongshanshanshan 您好,我在学习您的这个框架,发现还是有很多地方不明白,不知道您是否有空指导一下?可以给我邮件地址么?我的是 app_test@qq.com,谢谢

源码里 selocal EnableDelayedExpansion 这个命令写错了,应该是 setlocal EnableDelayedExpansion

用的是 Pycharm 搭的框架么?

runner.run(suit) 这一行报'\'super\' object has no attribute \'getattr\''是什么情况?

那些配置文件怎么用的,程序是怎么读取 element.xml 配置文件并找到相对应的测试用例开始运行的,能否详细说明一下

留名学习

感谢楼主, 最近正在研究怎么在 appium 基础上封装框架, 本来还只是想着把 locator 变量化和封装基本操作什么的, 看了你的文章才对框架要做的事情有了更多明确的概念. 顺便问下现在有 appium 的讨论群什么的嘛, 求加入

#47 楼 @apm017 你就叫 testerhome 的官方群吧

竟然用 python3.4😂 😂

@tongshanshanshan,楼主用的 python3.4 我将代码转换成 python2.7 版本以后,有两个问题请教一下楼主,1:在其它 py 文件中调用 mydriver.py 发现 driver 找不到 find_element_by_id;2:安装卸载 APP 的时候楼主为什么不适用 appium 提供的 api 来实现呢?

@tongshanshanshan ,请问 element(anctivityName ,elementName) 方法好像不能定位 dialog 弹框中的元素吧,对于这类元素有没有好点的定位方法?还有就是 element.xml 中的 anctivityName 是自己指定的吗,与工程源码中的 activity 没有关系吧

woyebuzhidaowoshishei 关闭了讨论 09月12日 17:56
jojotester [该话题已被删除] 中提及了此贴 11月10日 15:53
彭威 回复

请问现在你这个问题解决了吗?这个问题我也出现了,现在不知道怎么办,运行脚本就闪退,

正好在想怎么分层了,借鉴楼主的了,先模仿再去写自己的想法了,多谢

恒温 感谢 fir.im —— 结果公布 中提及了此贴 06月05日 13:20
老马 python appium UI 自动化测试框架讨论 中提及了此贴 02月01日 10:48
仅楼主可见

git 源码没有了~~~,哪个小伙伴有呀😀

隔壁老王 回复

兄弟,还有源码吗?git 地址失效了~~~~

楼主,github 链接失效了,给个源码链接可好,谢谢

driver.py 的源码能贴一下吗,单例模式的获取

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册