自動(dòng)化工具 用 Pytest+Appium+Allure 做 UI 自動(dòng)化的那些事~(有點(diǎn)干)

作者: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

Allure-pytest

Pytest

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é)們有幫助~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容