- Appium 不常见却好用的方法
- Pytest 与 Unittest 初始化上的区别
- 初始化实例
- Pytest 参数化方法
- Pytest 用例依赖关系
- Pytest 自定义标记,执行用例筛选作用
- _@_pytest.mark 模块给类或者函数加上标记,用于执行用例时进行筛选">1. 使用 _@_pytest.mark 模块给类或者函数加上标记,用于执行用例时进行筛选
- 2. 根据 test 节点选择用例
- 3. 使用 pytest hook 批量标记用例
- 用例错误处理截图,app 日志等
- Pytest 另一些 hook 的使用方法
- Pytest 一些常用方法
做 UI 自动化有段时间了,在社区也看了大量文章网上也搜集了不少资料资料,自己写代码、调试过程中中摸索了很多东西,踩了不少坑,这篇文章希望能给做 UI 自动化测试小伙伴们在 UI 自动化上有些许帮助。
文本主要介绍下 Pytest+Allure+Appium 记录一些过程和经历,一些好用的方法什么的,之前也没写过什么文章,文章可能有点干,看官们多喝水O(∩_∩) O~
主要用了啥:
- Python3
- Appium
- Allure-pytest
- Pytest
Appium 不常见却好用的方法
Appium 直接执行 adb shell 方法
# Appium 启动时增加 --relaxed-security 参数 Appium 即可执行类似adb shell的方法> appium -p 4723 --relaxed-security# 使用方法 def adb_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})return result['stdout']
Appium 直接截取元素图片的方法
element = self.driver.find_element_by_id('cn.xxxxxx:id/login_sign')pngbyte = element.screenshot_as_pngimage_data = BytesIO(pngbyte)img = Image.open(image_data)img.save('element.png')# 该方式能直接获取到登录按钮区域的截图
Appium 直接获取手机端日志
# 使用该方法后,手机端 logcat 缓存会清除归零,从新记录# 建议每条用例执行完执行一边清理,遇到错误再保存减少陈余 log 输出# Android logcat = self.driver.get_log('logcat')# iOS 需要安装 brew install libimobiledevice logcat = self.driver.get_log('syslog')# web 获取控制台日志 logcat = self.driver.get_log('browser')c = '\n'.join([i['message'] for i in logcat])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')with open('element.png', 'wb') as png1:png1.write(base64.b64decode(png))# 获取手机文件夹,导出的是zip文件 folder = driver.pull_folder('/sdcard/test')with open('test.zip', 'wb') as folder1:folder1.write(base64.b64decode(folder))# iOS# 需要安装 ifuse# > brew install ifuse 或者 > brew cask install osxfuse 或者 自行搜索安装方式driver.push_file('/Documents/xx/element.png', source_path='D:\works\element.png')# 向 App 沙盒中发送文件# iOS 8.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
class TestExample:def setup(self):print("setup class:TestStuff")def teardown(self):print ("teardown class:TestStuff")def setup_class(cls):print ("setup_class class:%s" % cls.__name__)def teardown_class(cls):print ("teardown_class class:%s" % cls.__name__)def setup_method(self, method):print ("setup_method method:%s" % method.__name__)def teardown_method(self, method):print ("teardown_method method:%s" % method.__name__)
2. 使用 pytest.fixture()
@pytest.fixture()def driver_setup(request):request.instance.Action = DriverClient().init_driver('android')def driver_teardown():request.instance.Action.quit()request.addfinalizer(driver_teardown)
初始化实例
1.setup_class 方式调用
class Singleton(object):"""单例ElementActions 为自己封装操作类"""Action = Nonedef __new__(cls, *args, **kw):if not hasattr(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 = Actionreturn cls._instanceclass DriverClient(Singleton):pass
测试用例中调用
class TestExample:def setup_class(cls):cls.Action = DriverClient().Actiondef teardown_class(cls):cls.Action.clear()def test_demo(self)self.Action.driver.launch_app()self.Action.set_text('123')
2.pytest.fixture() 方式调用
class DriverClient():def init_driver(self,device_name):desired_caps={}host = "http://localhost:4723/wd/hub"driver = webdriver.Remote(host, desired_caps)Action = ElementActions(driver, desired_caps)return Action# 该函数需要放置在 conftest.py, pytest 运行时会自动拾取 @pytest.fixture()def driver_setup(request):request.instance.Action = DriverClient().init_driver()def driver_teardown():request.instance.Action.clear()request.addfinalizer(driver_teardown)
测试用例中调用
#该装饰器会直接引入driver_setup函数 @user3res('driver_setup')class TestExample:def test_demo(self):self.Action.driver.launch_app()self.Action.set_text('123')
Pytest 参数化方法
1. 第一种方法 parametrize 装饰器参数化方法
@user4ize(('kewords'), [(u"小明"), (u"小红"), (u"小白")])def test_kewords(self,kewords):print(kewords)# 多个参数 @user5ize("test_input,expected", [("3+5", 8),("2+4", 6),("6*9", 42),])def test_eval(test_input, expected):assert eval(test_input) == expected
2. 第二种方法,使用 pytest hook 批量加参数化
# conftest.py def pytest_generate_tests(metafunc):"""使用 hook 给用例加加上参数metafunc.cls.params 对应类中的 params 参数"""try:if metafunc.cls.params and metafunc.function.__name__ in metafunc.cls.params: ## 对应 TestClass paramsfuncarglist = metafunc.cls.params[metafunc.function.__name__]argnames = list(funcarglist[0])metafunc.parametrize(argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist])except AttributeError:pass# test_demo.py class TestClass:""":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}],}def test_a(self, a, b):assert a == bdef test_b(self, a, b):assert a == b
Pytest 用例依赖关系
使用 pytest-dependency 库可以创造依赖关系
当上层用例没通过,后续依赖关系用例将直接跳过,可以跨 Class 类筛选
如果需要跨. py 文件运行 需要将 site-packages/pytest_dependency.py 文件的
class DependencyManager(object):"""Dependency manager, stores the results of tests."""ScopeCls = {'module':pytest.Module, 'session':pytest.Session}@classmethoddef getManager(cls, item, scope='session'): # 这里修改成 session
如果
> pip install pytest-dependencyclass TestExample(object):@user7cy()def test_a(self):assert False@user8cy()def test_b(self):assert False@user9cy(depends=["TestExample::test_a"])def test_c(self):# TestExample::test_a 没通过则不执行该条用例# 可以跨 Class 筛选print("Hello I am in test_c")@user10cy(depends=["TestExample::test_a","TestExample::test_b"])def test_d(self):print("Hello I am in test_d")pytest -v test_demo.py2 failed- test_1.py:6 TestExample.test_a- test_1.py:10 TestExample.test_b2 skipped
Pytest 自定义标记,执行用例筛选作用
1. 使用 _@_pytest.mark 模块给类或者函数加上标记,用于执行用例时进行筛选
@pytest.mark.webtestdef test_webtest():pass@pytest.mark.apitestclass TestExample(object):def test_a(self):pass@pytest.mark.httptestdef test_b(self):pass
仅执行标记 webtest 的用例
pytest -v -m webtestResults (0.03s):1 passed2 deselected
执行标记多条用例
pytest -v -m "webtest or apitest"Results (0.05s):3 passed
仅不执行标记 webtest 的用例
pytest -v -m "not webtest"Results (0.04s):2 passed1 deselected
不执行标记多条用例
pytest -v -m "not webtest and not apitest"Results (0.02s):3 deselected
2. 根据 test 节点选择用例
pytest -v Test_example.py::TestClass::test_apytest -v Test_example.py::TestClasspytest -v Test_example.py Test_example2.py
3. 使用 pytest hook 批量标记用例
# conftet.pydef pytest_collection_modifyitems(items):"""获取每个函数名字,当用例中含有该字符则打上标记"""for item in items:if "http" in item.nodeid:item.add_marker(pytest.mark.http)elif "api" in item.nodeid:item.add_marker(pytest.mark.api)class TestExample(object):def test_api_1(self):passdef test_api_2(self):passdef test_http_1(self):passdef test_http_2(self):passdef test_demo(self):pass
仅执行标记 api 的用例
pytest -v -m apiResults (0.03s):2 passed3 deselected可以看到使用批量标记之后,测试用例中只执行了带有 api 的方法
用例错误处理截图,app 日志等
- 第一种使用 python 函数装饰器方法
def monitorapp(function):"""用例装饰器,截图,日志,是否跳过等获取系统log,Android logcat、ios 使用syslog"""@wraps(function)def wrapper(self, *args, **kwargs):try:allure.dynamic.description('用例开始时间:{}'.format(datetime.datetime.now()))function(self, *args, **kwargs)self.Action.driver.get_log('logcat')except Exception as E: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'] for i in logcat])allure.attach(c, 'APPlog', allure.attachment_type.TEXT)raise Efinally:if self.Action.get_app_pid() != self.Action.Apppid:raise Exception('设备进程 ID 变化,可能发生崩溃')return wrapper
- 第二种使用 pytest hook 方法 (与方法一选一)
@pytest.hookimpl(tryfirst=True, hookwrapper=True)def pytest_runtest_makereport(item, call):Action = DriverClient().Actionoutcome = yieldrep = outcome.get_result()if rep.when == "call" and rep.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'] for i in logcat])allure.attach(c, 'APPlog', allure.attachment_type.TEXT)if Action.get_app_pid() != Action.apppid:raise Exception('设备进程 ID 变化,可能发生崩溃')
Pytest 另一些 hook 的使用方法
1. 自定义 Pytest 参数
> pytest -s -all# content of conftest.pydef pytest_addoption(parser):"""自定义参数"""parser.addoption("--all", action="store_true",default="type1",help="run all combinations")def pytest_generate_tests(metafunc):if 'param' in metafunc.fixturenames:if metafunc.config.option.all: # 这里能获取到自定义参数paramlist = [1,2,3]else:paramlist = [1,2,4]metafunc.parametrize("param",paramlist) # 给用例加参数化# 怎么在测试用例中获取自定义参数呢# content of conftest.pydef pytest_addoption(parser):"""自定义参数"""parser.addoption("--cmdopt", action="store_true",default="type1",help="run all combinations")@pytest.fixturedef cmdopt(request):return request.config.getoption("--cmdopt")# test_sample.py 测试用例中使用def test_sample(cmdopt):if cmdopt == "type1":print("first")elif cmdopt == "type2":print("second")assert 1> pytest -q --cmdopt=type2second.1 passed in 0.09 seconds
2.Pytest 过滤测试目录
#过滤 pytest 需要执行的文件夹或者文件名字def pytest_ignore_collect(path,config):if 'logcat' in path.dirname:return True #返回 True 则该文件不执行
Pytest 一些常用方法
Pytest 用例优先级(比如优先登录什么的)
> pip install pytest-ordering@pytest.mark.run(order=1)class TestExample:def test_a(self):
Pytest 用例失败重试
#原始方法pytet -s test_demo.pypytet -s --lf test_demo.py #第二次执行时,只会执行失败的用例pytet -s --ll test_demo.py #第二次执行时,会执行所有用例,但会优先执行失败用例#使用第三方插件pip install pytest-rerunfailures #使用插件pytest --reruns 2 # 失败case重试两次
Pytest 其他常用参数
pytest --maxfail=10 #失败超过10次则停止运行pytest -x test_demo.py #出现失败则停止
下一篇文章将计划实战用 Pytest hook 函数运行 yaml 文件来驱动 Appium 做自动化测试,并提供测试源码~
https://testerhome.com/topics/19327
