自动化工具 用 Pytest+Appium+Allure 做 UI 自动化的那些事~(有点干)

作者:xiaoj (YueChen)

做 UI 自动化有段时间了,在社区也看了大量文章网上也搜集了不少资料资料,自己写代码、调试过程中中摸索了很多东西,踩了不少坑,这篇文章希望能给做 UI 自动化测试小伙伴们在 UI 自动化上有些许帮助。

文本主要介绍下 Pytest+Allure+Appium 记录一些过程和经历,一些好用的方法什么的,之前也没写过什么文章,文章可能有点干,看官们多喝水


 O(∩_∩)O~

主要用了啥:

Python3

Appium

Allure-pytest

Pytest

Appium 不常见却好用的方法

Appium 直接执行 adb shell 方法

#Appium启动时增加--relaxed-security参数Appium即可执行类似adbshell的方法

>appium-p4723--relaxed-security

#使用方法

defadb_shell(self,command,args,includeStderr=False):

"""

appium --relaxed-security 方式启动

adb_shell('ps',['|','grep','android'])

:param command:命令

:param args:参数

:param includeStderr: 为 True 则抛异常

:return:

"""

result=self.driver.execute_script('mobile:shell',{

'command':command,

'args':args,

'includeStderr':includeStderr,

'timeout':5000

})

returnresult['stdout']

Appium 直接截取元素图片的方法

element=self.driver.find_element_by_id('cn.xxxxxx:id/login_sign')

pngbyte=element.screenshot_as_png

image_data=BytesIO(pngbyte)

img=Image.open(image_data)

img.save('element.png')

#该方式能直接获取到登录按钮区域的截图

Appium 直接获取手机端日志

#使用该方法后,手机端logcat缓存会清除归零,从新记录

#建议每条用例执行完执行一边清理,遇到错误再保存减少陈余log输出

#Android

logcat=self.driver.get_log('logcat')

#iOS需要安装brewinstalllibimobiledevice

logcat=self.driver.get_log('syslog')

#web获取控制台日志

logcat=self.driver.get_log('browser')

c='\n'.join([i['message']foriinlogcat])

allure.attach(c,'APPlog',allure.attachment_type.TEXT)

#写入到allure测试报告中

Appium 直接与设备传输文件

#发送文件

#Android

driver.push_file('/sdcard/element.png',source_path='D:\works\element.png')

#获取手机文件

png=driver.pull_file('/sdcard/element.png')

withopen('element.png','wb')aspng1:

png1.write(base64.b64decode(png))

#获取手机文件夹,导出的是zip文件

folder=driver.pull_folder('/sdcard/test')

withopen('test.zip','wb')asfolder1:

folder1.write(base64.b64decode(folder))

#iOS

#需要安装ifuse

#>brewinstallifuse或者>brewcaskinstallosxfuse或者自行搜索安装方式

driver.push_file('/Documents/xx/element.png',source_path='D:\works\element.png')

#向App沙盒中发送文件

#iOS8.3之后需要应用开启UIFileSharingEnabled权限不然会报错

bundleId='cn.xxx.xxx'#APP名字

driver.push_file('@{bundleId}:Documents/xx/element.png'.format(bundleId=bundleId),source_path='D:\works\element.png')

Pytest 与 Unittest 初始化上的区别

很多人都使用过 unitest 先说一下 pytest 和 unitest 在 Hook method上的一些区别

1.Pytest 与 unitest 类似,有些许区别,以下是 Pytest

classTestExample:

defsetup(self):

print("setup            class:TestStuff")

defteardown(self):

print("teardown          class:TestStuff")

defsetup_class(cls):

print("setup_class      class:%s"%cls.__name__)

defteardown_class(cls):

print("teardown_class    class:%s"%cls.__name__)

defsetup_method(self,method):

print("setup_method      method:%s"%method.__name__)

defteardown_method(self,method):

print("teardown_method  method:%s"%method.__name__)

2.使用 pytest.fixture()

@pytest.fixture()

defdriver_setup(request):

request.instance.Action=DriverClient().init_driver('android')

defdriver_teardown():

request.instance.Action.quit()

request.addfinalizer(driver_teardown)

初始化实例

1.setup_class 方式调用

classSingleton(object):

"""单例

ElementActions 为自己封装操作类"""

Action=None

def__new__(cls,*args,**kw):

ifnothasattr(cls,'_instance'):

desired_caps={}

host="http://localhost:4723/wd/hub"

driver=webdriver.Remote(host,desired_caps)

Action=ElementActions(driver,desired_caps)

orig=super(Singleton,cls)

cls._instance=orig.__new__(cls,*args,**kw)

cls._instance.Action=Action

returncls._instance

classDriverClient(Singleton):

pass

测试用例中调用

classTestExample:

defsetup_class(cls):

cls.Action=DriverClient().Action

defteardown_class(cls):

cls.Action.clear()

deftest_demo(self)

self.Action.driver.launch_app()

self.Action.set_text('123')

2.pytest.fixture() 方式调用

classDriverClient():

definit_driver(self,device_name):

desired_caps={}

host="http://localhost:4723/wd/hub"

driver=webdriver.Remote(host,desired_caps)

Action=ElementActions(driver,desired_caps)

returnAction

#该函数需要放置在conftest.py,pytest运行时会自动拾取

@pytest.fixture()

defdriver_setup(request):

request.instance.Action=DriverClient().init_driver()

defdriver_teardown():

request.instance.Action.clear()

request.addfinalizer(driver_teardown)

测试用例中调用

#该装饰器会直接引入driver_setup函数

@pytest.mark.usefixtures('driver_setup')

classTestExample:

deftest_demo(self):

self.Action.driver.launch_app()

self.Action.set_text('123')

Pytest 参数化方法

1.第一种方法 parametrize 装饰器参数化方法

@pytest.mark.parametrize(('kewords'),[(u"小明"),(u"小红"),(u"小白")])

deftest_kewords(self,kewords):

print(kewords)

#多个参数

@pytest.mark.parametrize("test_input,expected",[

("3+5",8),

("2+4",6),

("6*9",42),

])

deftest_eval(test_input,expected):

asserteval(test_input)==expected

2.第二种方法,使用 pytest hook 批量加参数化

#conftest.py

defpytest_generate_tests(metafunc):

"""

使用 hook 给用例加加上参数

metafunc.cls.params 对应类中的 params 参数

"""

try:

ifmetafunc.cls.paramsandmetafunc.function.__name__inmetafunc.cls.params:##对应TestClassparams

funcarglist=metafunc.cls.params[metafunc.function.__name__]

argnames=list(funcarglist[0])

metafunc.parametrize(argnames,[[funcargs[name]fornameinargnames]forfuncargsinfuncarglist])

exceptAttributeError:

pass

#test_demo.py

classTestClass:

"""

:params 对应 hook 中 metafunc.cls.params

"""

#params=Parameterize('TestClass.yaml').getdata()

params={

'test_a':[{'a':1,'b':2},{'a':1,'b':2}],

'test_b':[{'a':1,'b':2},{'a':1,'b':2}],

}

deftest_a(self,a,b):

asserta==b

deftest_b(self,a,b):

asserta==b

Pytest 用例依赖关系

使用 pytest-dependency 库可以创造依赖关系

当上层用例没通过,后续依赖关系用例将直接跳过,可以跨 Class 类筛选

如果需要跨.py 文件运行 需要将 site-packages/pytest_dependency.py 文件的

classDependencyManager(object):

"""Dependency manager, stores the results of tests.

"""

ScopeCls={'module':pytest.Module,'session':pytest.Session}

@classmethod

defgetManager(cls,item,scope='session'):#这里修改成session

如果

>pipinstallpytest-dependency

classTestExample(object):

@pytest.mark.dependency()

deftest_a(self):

assertFalse

@pytest.mark.dependency()

deftest_b(self):

assertFalse

@pytest.mark.dependency(depends=["TestExample::test_a"])

deftest_c(self):

#TestExample::test_a没通过则不执行该条用例

#可以跨Class筛选

print("Hello I am in test_c")

@pytest.mark.dependency(depends=["TestExample::test_a","TestExample::test_b"])

deftest_d(self):

print("Hello I am in test_d")

pytest-vtest_demo.py

2failed

-test_1.py:6TestExample.test_a

-test_1.py:10TestExample.test_b

2skipped

Pytest 自定义标记,执行用例筛选作用

1.使用 @pytest.mark 模块给类或者函数加上标记,用于执行用例时进行筛选

@pytest.mark.webtest

deftest_webtest():

pass

@pytest.mark.apitest

classTestExample(object):

deftest_a(self):

pass

@pytest.mark.httptest

deftest_b(self):

pass

仅执行标记 webtest 的用例

pytest-v-mwebtest

Results(0.03s):

1passed

2deselected

执行标记多条用例

pytest-v-m"webtest or apitest"

Results(0.05s):

3passed

仅不执行标记 webtest 的用例

pytest-v-m"not webtest"

Results(0.04s):

2passed

1deselected

不执行标记多条用例

pytest-v-m"not webtest and not apitest"

Results(0.02s):

3deselected

2.根据 test 节点选择用例

pytest-vTest_example.py::TestClass::test_a

pytest-vTest_example.py::TestClass

pytest-vTest_example.pyTest_example2.py

3.使用 pytest hook 批量标记用例

#conftet.py

defpytest_collection_modifyitems(items):

"""

获取每个函数名字,当用例中含有该字符则打上标记

"""

foriteminitems:

if"http"initem.nodeid:

item.add_marker(pytest.mark.http)

elif"api"initem.nodeid:

item.add_marker(pytest.mark.api)

classTestExample(object):

deftest_api_1(self):

pass

deftest_api_2(self):

pass

deftest_http_1(self):

pass

deftest_http_2(self):

pass

deftest_demo(self):

pass

仅执行标记 api 的用例

pytest-v-mapi

Results(0.03s):

2passed

3deselected

可以看到使用批量标记之后,测试用例中只执行了带有api的方法

用例错误处理截图,app 日志等

1.第一种使用 python 函数装饰器方法

defmonitorapp(function):

"""

用例装饰器,截图,日志,是否跳过等

获取系统log,Android logcat、ios 使用syslog

"""

@wraps(function)

defwrapper(self,*args,**kwargs):

try:

allure.dynamic.description('用例开始时间:{}'.format(datetime.datetime.now()))

function(self,*args,**kwargs)

self.Action.driver.get_log('logcat')

exceptExceptionasE:

f=self.Action.driver.get_screenshot_as_png()

allure.attach(f,'失败截图',allure.attachment_type.PNG)

logcat=self.Action.driver.get_log('logcat')

c='\n'.join([i['message']foriinlogcat])

allure.attach(c,'APPlog',allure.attachment_type.TEXT)

raiseE

finally:

ifself.Action.get_app_pid()!=self.Action.Apppid:

raiseException('设备进程ID变化,可能发生崩溃')

returnwrapper

2.第二种使用 pytest hook 方法 (与方法一选一)

@pytest.hookimpl(tryfirst=True,hookwrapper=True)

defpytest_runtest_makereport(item,call):

Action=DriverClient().Action

outcome=yield

rep=outcome.get_result()

ifrep.when=="call"andrep.failed:

f=Action.driver.get_screenshot_as_png()

allure.attach(f,'失败截图',allure.attachment_type.PNG)

logcat=Action.driver.get_log('logcat')

c='\n'.join([i['message']foriinlogcat])

allure.attach(c,'APPlog',allure.attachment_type.TEXT)

ifAction.get_app_pid()!=Action.apppid:

raiseException('设备进程ID变化,可能发生崩溃')

Pytest 另一些 hook 的使用方法

1.自定义 Pytest 参数

>pytest-s-all

#contentofconftest.py

defpytest_addoption(parser):

"""

自定义参数

"""

parser.addoption("--all",action="store_true",default="type1",help="run all combinations")

defpytest_generate_tests(metafunc):

if'param'inmetafunc.fixturenames:

ifmetafunc.config.option.all:#这里能获取到自定义参数

paramlist=[1,2,3]

else:

paramlist=[1,2,4]

metafunc.parametrize("param",paramlist)#给用例加参数化

#怎么在测试用例中获取自定义参数呢

#contentofconftest.py

defpytest_addoption(parser):

"""

自定义参数

"""

parser.addoption("--cmdopt",action="store_true",default="type1",help="run all combinations")

@pytest.fixture

defcmdopt(request):

returnrequest.config.getoption("--cmdopt")

#test_sample.py测试用例中使用

deftest_sample(cmdopt):

ifcmdopt=="type1":

print("first")

elifcmdopt=="type2":

print("second")

assert1

>pytest-q--cmdopt=type2

second

.

1passedin0.09seconds

2.Pytest 过滤测试目录

#过滤pytest需要执行的文件夹或者文件名字

defpytest_ignore_collect(path,config):

if'logcat'inpath.dirname:

returnTrue#返回True则该文件不执行

Pytest 一些常用方法

Pytest 用例优先级(比如优先登录什么的)

>pipinstallpytest-ordering

@pytest.mark.run(order=1)

classTestExample:

deftest_a(self):

Pytest 用例失败重试

#原始方法

pytet-stest_demo.py

pytet-s--lftest_demo.py#第二次执行时,只会执行失败的用例

pytet-s--lltest_demo.py#第二次执行时,会执行所有用例,但会优先执行失败用例

#使用第三方插件

pipinstallpytest-rerunfailures#使用插件

pytest--reruns2#失败case重试两次

Pytest 其他常用参数

pytest--maxfail=10#失败超过10次则停止运行

pytest-xtest_demo.py#出现失败则停止

希望对测试同学们有帮助~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容