作者:xiaoj (YueChen)
做 UI 自動(dòng)化有段時(shí)間了,在社區(qū)也看了大量文章網(wǎng)上也搜集了不少資料資料,自己寫(xiě)代碼、調(diào)試過(guò)程中中摸索了很多東西,踩了不少坑,這篇文章希望能給做 UI 自動(dòng)化測(cè)試小伙伴們?cè)?UI 自動(dòng)化上有些許幫助。
文本主要介紹下 Pytest+Allure+Appium 記錄一些過(guò)程和經(jīng)歷,一些好用的方法什么的,之前也沒(méi)寫(xiě)過(guò)什么文章,文章可能有點(diǎn)干,看官們多喝水
?O(∩_∩)O~
主要用了啥:
Python3
Appium 不常見(jiàn)卻好用的方法
Appium 直接執(zhí)行 adb shell 方法
#Appium啟動(dòng)時(shí)增加--relaxed-security參數(shù)Appium即可執(zhí)行類(lèi)似adbshell的方法
>appium-p4723--relaxed-security
#使用方法
defadb_shell(self,command,args,includeStderr=False):
"""
appium --relaxed-security 方式啟動(dòng)
adb_shell('ps',['|','grep','android'])
:param command:命令
:param args:參數(shù)
: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')
#該方式能直接獲取到登錄按鈕區(qū)域的截圖
Appium 直接獲取手機(jī)端日志
#使用該方法后,手機(jī)端logcat緩存會(huì)清除歸零,從新記錄
#建議每條用例執(zhí)行完執(zhí)行一邊清理,遇到錯(cuò)誤再保存減少陳余l(xiāng)og輸出
#Android
logcat=self.driver.get_log('logcat')
#iOS需要安裝brewinstalllibimobiledevice
logcat=self.driver.get_log('syslog')
#web獲取控制臺(tái)日志
logcat=self.driver.get_log('browser')
c='\n'.join([i['message']foriinlogcat])
allure.attach(c,'APPlog',allure.attachment_type.TEXT)
#寫(xiě)入到allure測(cè)試報(bào)告中
Appium 直接與設(shè)備傳輸文件
#發(fā)送文件
#Android
driver.push_file('/sdcard/element.png',source_path='D:\works\element.png')
#獲取手機(jī)文件
png=driver.pull_file('/sdcard/element.png')
withopen('element.png','wb')aspng1:
png1.write(base64.b64decode(png))
#獲取手機(jī)文件夾,導(dǎo)出的是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沙盒中發(fā)送文件
#iOS8.3之后需要應(yīng)用開(kāi)啟UIFileSharingEnabled權(quán)限不然會(huì)報(bào)錯(cuò)
bundleId='cn.xxx.xxx'#APP名字
driver.push_file('@{bundleId}:Documents/xx/element.png'.format(bundleId=bundleId),source_path='D:\works\element.png')
Pytest 與 Unittest 初始化上的區(qū)別
很多人都使用過(guò) unitest 先說(shuō)一下 pytest 和 unitest 在 Hook method上的一些區(qū)別
1.Pytest 與 unitest 類(lèi)似,有些許區(qū)別,以下是 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)
初始化實(shí)例
1.setup_class 方式調(diào)用
classSingleton(object):
"""單例
ElementActions 為自己封裝操作類(lèi)"""
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
測(cè)試用例中調(diào)用
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() 方式調(diào)用
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
#該函數(shù)需要放置在conftest.py,pytest運(yùn)行時(shí)會(huì)自動(dòng)拾取
@pytest.fixture()
defdriver_setup(request):
request.instance.Action=DriverClient().init_driver()
defdriver_teardown():
request.instance.Action.clear()
request.addfinalizer(driver_teardown)
測(cè)試用例中調(diào)用
#該裝飾器會(huì)直接引入driver_setup函數(shù)
@pytest.mark.usefixtures('driver_setup')
classTestExample:
deftest_demo(self):
self.Action.driver.launch_app()
self.Action.set_text('123')
Pytest 參數(shù)化方法
1.第一種方法 parametrize 裝飾器參數(shù)化方法
@pytest.mark.parametrize(('kewords'),[(u"小明"),(u"小紅"),(u"小白")])
deftest_kewords(self,kewords):
print(kewords)
#多個(gè)參數(shù)
@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 批量加參數(shù)化
#conftest.py
defpytest_generate_tests(metafunc):
"""
使用 hook 給用例加加上參數(shù)
metafunc.cls.params 對(duì)應(yīng)類(lèi)中的 params 參數(shù)
"""
try:
ifmetafunc.cls.paramsandmetafunc.function.__name__inmetafunc.cls.params:##對(duì)應(yīng)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 對(duì)應(yīng) 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 用例依賴(lài)關(guān)系
使用 pytest-dependency 庫(kù)可以創(chuàng)造依賴(lài)關(guān)系
當(dāng)上層用例沒(méi)通過(guò),后續(xù)依賴(lài)關(guān)系用例將直接跳過(guò),可以跨 Class 類(lèi)篩選
如果需要跨.py 文件運(yùn)行 需要將 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沒(méi)通過(guò)則不執(zhí)行該條用例
#可以跨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 自定義標(biāo)記,執(zhí)行用例篩選作用
1.使用?@pytest.mark?模塊給類(lèi)或者函數(shù)加上標(biāo)記,用于執(zhí)行用例時(shí)進(jìn)行篩選
@pytest.mark.webtest
deftest_webtest():
pass
@pytest.mark.apitest
classTestExample(object):
deftest_a(self):
pass
@pytest.mark.httptest
deftest_b(self):
pass
僅執(zhí)行標(biāo)記 webtest 的用例
pytest-v-mwebtest
Results(0.03s):
1passed
2deselected
執(zhí)行標(biāo)記多條用例
pytest-v-m"webtest or apitest"
Results(0.05s):
3passed
僅不執(zhí)行標(biāo)記 webtest 的用例
pytest-v-m"not webtest"
Results(0.04s):
2passed
1deselected
不執(zhí)行標(biāo)記多條用例
pytest-v-m"not webtest and not apitest"
Results(0.02s):
3deselected
2.根據(jù) test 節(jié)點(diǎn)選擇用例
pytest-vTest_example.py::TestClass::test_a
pytest-vTest_example.py::TestClass
pytest-vTest_example.pyTest_example2.py
3.使用 pytest hook 批量標(biāo)記用例
#conftet.py
defpytest_collection_modifyitems(items):
"""
獲取每個(gè)函數(shù)名字,當(dāng)用例中含有該字符則打上標(biāo)記
"""
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
僅執(zhí)行標(biāo)記 api 的用例
pytest-v-mapi
Results(0.03s):
2passed
3deselected
可以看到使用批量標(biāo)記之后,測(cè)試用例中只執(zhí)行了帶有api的方法
用例錯(cuò)誤處理截圖,app 日志等
1.第一種使用 python 函數(shù)裝飾器方法
defmonitorapp(function):
"""
用例裝飾器,截圖,日志,是否跳過(guò)等
獲取系統(tǒng)log,Android logcat、ios 使用syslog
"""
@wraps(function)
defwrapper(self,*args,**kwargs):
try:
allure.dynamic.description('用例開(kāi)始時(shí)間:{}'.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('設(shè)備進(jìn)程ID變化,可能發(fā)生崩潰')
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('設(shè)備進(jìn)程ID變化,可能發(fā)生崩潰')
Pytest 另一些 hook 的使用方法
1.自定義 Pytest 參數(shù)
>pytest-s-all
#contentofconftest.py
defpytest_addoption(parser):
"""
自定義參數(shù)
"""
parser.addoption("--all",action="store_true",default="type1",help="run all combinations")
defpytest_generate_tests(metafunc):
if'param'inmetafunc.fixturenames:
ifmetafunc.config.option.all:#這里能獲取到自定義參數(shù)
paramlist=[1,2,3]
else:
paramlist=[1,2,4]
metafunc.parametrize("param",paramlist)#給用例加參數(shù)化
#怎么在測(cè)試用例中獲取自定義參數(shù)呢
#contentofconftest.py
defpytest_addoption(parser):
"""
自定義參數(shù)
"""
parser.addoption("--cmdopt",action="store_true",default="type1",help="run all combinations")
@pytest.fixture
defcmdopt(request):
returnrequest.config.getoption("--cmdopt")
#test_sample.py測(cè)試用例中使用
deftest_sample(cmdopt):
ifcmdopt=="type1":
print("first")
elifcmdopt=="type2":
print("second")
assert1
>pytest-q--cmdopt=type2
second
.
1passedin0.09seconds
2.Pytest 過(guò)濾測(cè)試目錄
#過(guò)濾pytest需要執(zhí)行的文件夾或者文件名字
defpytest_ignore_collect(path,config):
if'logcat'inpath.dirname:
returnTrue#返回True則該文件不執(zhí)行
Pytest 一些常用方法
Pytest 用例優(yōu)先級(jí)(比如優(yōu)先登錄什么的)
>pipinstallpytest-ordering
@pytest.mark.run(order=1)
classTestExample:
deftest_a(self):
Pytest 用例失敗重試
#原始方法
pytet-stest_demo.py
pytet-s--lftest_demo.py#第二次執(zhí)行時(shí),只會(huì)執(zhí)行失敗的用例
pytet-s--lltest_demo.py#第二次執(zhí)行時(shí),會(huì)執(zhí)行所有用例,但會(huì)優(yōu)先執(zhí)行失敗用例
#使用第三方插件
pipinstallpytest-rerunfailures#使用插件
pytest--reruns2#失敗case重試兩次
Pytest 其他常用參數(shù)
pytest--maxfail=10#失敗超過(guò)10次則停止運(yùn)行
pytest-xtest_demo.py#出現(xiàn)失敗則停止
希望對(duì)測(cè)試同學(xué)們有幫助~