作者: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#出现失败则停止
希望对测试同学们有帮助~