自动化工具 用 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#出现失败则停止

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

你可能感兴趣的:(自动化工具 用 Pytest+Appium+Allure 做 UI 自动化的那些事~(有点干))