用 Pytest+Appium+Allure 做 UI 自动化测试的那些事儿
前言
做 UI 自动化测试有段时间了,
社区看了大量文章,也在网上搜集了不少资料,加上自己写代码、调试过程中摸索了很多东西,踩了不少坑,才有了这篇文章。希望能给做 UI
自动化测试小伙伴们提供些许帮助。
文本主要介绍用 Pytest+Allure+Appium 实现 UI
自动化测试过程中的一些好用的方法和避坑经验。文章可能有点干,看官们多喝水!O(∩_∩)O~
主要用了啥:
-
Python3
-
Appium
-
Allure-pytest
-
Pytest
Appium 不常见却好用的方法
1. Appium 直接执行 adb shell 方法
1. # Appium 启动时增加 --relaxed-security 参数 Appium 即可执行类似adb shell的方法
2. > appium -p 4723 --relaxed-security
1. # 使用方法
2. def adb_shell(self, command, args, includeStderr=False):
3. """
4. appium --relaxed-security 方式启动
5. adb_shell('ps',['|','grep','android'])
6.
7. :param command:命令
8. :param args:参数
9. :param includeStderr: 为 True 则抛异常
10. :return:
11. """
12. result = self.driver.execute_script('mobile: shell', {
13. 'command': command,
14. 'args': args,
15. 'includeStderr': includeStderr,
16. 'timeout': 5000
17. })
18. return result['stdout']
2. Appium 直接截取元素图片的方法
1. element = self.driver.find_element_by_id('cn.xxxxxx:id/login_sign')
2. pngbyte = element.screenshot_as_png
3. image_data = BytesIO(pngbyte)
4. img = Image.open(image_data)
5. img.save('element.png')
6. # 该方式能直接获取到登录按钮区域的截图
3. Appium 直接获取手机端日志
1. # 使用该方法后,手机端 logcat 缓存会清除归零,从新记录
2. # 建议每条用例执行完执行一边清理,遇到错误再保存减少陈余 log 输出
3. # Android
4. logcat = self.driver.get_log('logcat')
5.
6. # iOS 需要安装 brew install libimobiledevice
7. logcat = self.driver.get_log('syslog')
8.
9. # web 获取控制台日志
10. logcat = self.driver.get_log('browser')
11.
12. c = '\n'.join([i['message'] for i in logcat])
13. allure.attach(c, 'APPlog', allure.attachment_type.TEXT)
14. #写入到 allure 测试报告中
4. Appium 直接与设备传输文件
1. # 发送文件
2. #Android
3. driver.push_file('/sdcard/element.png', source_path='D:\works\element.png')
4.
5. # 获取手机文件
6. png = driver.pull_file('/sdcard/element.png')
7. with open('element.png', 'wb') as png1:
8. png1.write(base64.b64decode(png))
9.
10. # 获取手机文件夹,导出的是zip文件
11. folder = driver.pull_folder('/sdcard/test')
12. with open('test.zip', 'wb') as folder1:
13. folder1.write(base64.b64decode(folder))
14.
15. # iOS
16. # 需要安装 ifuse
17. # > brew install ifuse 或者 > brew cask install osxfuse 或者 自行搜索安装方式
18.
19. driver.push_file('/Documents/xx/element.png', source_path='D:\works\element.png')
20.
21. # 向 App 沙盒中发送文件
22. # iOS 8.3 之后需要应用开启 UIFileSharingEnabled 权限不然会报错
23. bundleId = 'cn.xxx.xxx' # APP名字
24. 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
1. class TestExample:
2. def setup(self):
3. print("setup class:TestStuff")
4.
5. def teardown(self):
6. print ("teardown class:TestStuff")
7.
8. def setup_class(cls):
9. print ("setup_class class:%s" % cls.__name__)
10.
11. def teardown_class(cls):
12. print ("teardown_class class:%s" % cls.__name__)
13.
14. def setup_method(self, method):
15. print ("setup_method method:%s" % method.__name__)
16.
17. def teardown_method(self, method):
18. print ("teardown_method method:%s" % method.__name__)
2. 使用 pytest.fixture()
1. @pytest.fixture()
2. def driver_setup(request):
3. request.instance.Action = DriverClient().init_driver('android')
4. def driver_teardown():
5. request.instance.Action.quit()
6. request.addfinalizer(driver_teardown)
初始化实例
1. setup_class 方式调用
1. class Singleton(object):
2. """单例
3. ElementActions 为自己封装操作类"""
4. Action = None
5.
6. def __new__(cls, *args, **kw):
7. if not hasattr(cls, '_instance'):
8. desired_caps={}
9. host = "http://localhost:4723/wd/hub"
10. driver = webdriver.Remote(host, desired_caps)
11. Action = ElementActions(driver, desired_caps)
12. orig = super(Singleton, cls)
13. cls._instance = orig.__new__(cls, *args, **kw)
14. cls._instance.Action = Action
15. return cls._instance
16.
17. class DriverClient(Singleton):
18. pass
测试用例中调用
1. class TestExample:
2. def setup_class(cls):
3. cls.Action = DriverClient().Action
4.
5. def teardown_class(cls):
6. cls.Action.clear()
7.
8.
9. def test_demo(self)
10. self.Action.driver.launch_app()
11. self.Action.set_text('123')
2. pytest.fixture() 方式调用
1. class DriverClient():
2.
3. def init_driver(self,device_name):
4. desired_caps={}
5. host = "http://localhost:4723/wd/hub"
6. driver = webdriver.Remote(host, desired_caps)
7. Action = ElementActions(driver, desired_caps)
8. return Action
9.
10.
11.
12. # 该函数需要放置在 conftest.py, pytest 运行时会自动拾取
13. @pytest.fixture()
14. def driver_setup(request):
15. request.instance.Action = DriverClient().init_driver()
16. def driver_teardown():
17. request.instance.Action.clear()
18. request.addfinalizer(driver_teardown)
测试用例中调用
1. #该装饰器会直接引入driver_setup函数
2. @pytest.mark.usefixtures('driver_setup')
3. class TestExample:
4.
5. def test_demo(self):
6. self.Action.driver.launch_app()
7. self.Action.set_text('123')
Pytest 参数化方法
1. 第一种方法 parametrize 装饰器参数化方法
1. @pytest.mark.parametrize(('kewords'), [(u"小明"), (u"小红"), (u"小白")])
2. def test_kewords(self,kewords):
3. print(kewords)
4.
5. # 多个参数
6. @pytest.mark.parametrize("test_input,expected", [
7. ("3+5", 8),
8. ("2+4", 6),
9. ("6*9", 42),
10. ])
11. def test_eval(test_input, expected):
12. assert eval(test_input) == expected
2.第二种方法,使用 pytest hook 批量加参数化
1. # conftest.py
2. def pytest_generate_tests(metafunc):
3. """
4. 使用 hook 给用例加加上参数
5. metafunc.cls.params 对应类中的 params 参数
6.
7. """
8. try:
9. if metafunc.cls.params and metafunc.function.__name__ in metafunc.cls.params: ## 对应 TestClass params
10. funcarglist = metafunc.cls.params[metafunc.function.__name__]
11. argnames = list(funcarglist[0])
12. metafunc.parametrize(argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist])
13. except AttributeError:
14. pass
15.
16. # test_demo.py
17. class TestClass:
18. """
19. :params 对应 hook 中 metafunc.cls.params
20. """
21. # params = Parameterize('TestClass.yaml').getdata()
22.
23. params = {
24. 'test_a': [{'a': 1, 'b': 2}, {'a': 1, 'b': 2}],
25. 'test_b': [{'a': 1, 'b': 2}, {'a': 1, 'b': 2}],
26. }
27. def test_a(self, a, b):
28. assert a == b
29. def test_b(self, a, b):
30. assert a == b
Pytest 用例依赖关系
使用 pytest-dependency 库可以创造依赖关系。
当上层用例没通过,后续依赖关系用例将直接跳过,可以跨 Class 类筛选。如果需要跨 .py
文件运行 需要将 site- packages/pytest_dependency.py
文件的
1. class DependencyManager(object):
2. """Dependency manager, stores the results of tests.
3. """
4.
5. ScopeCls = {'module':pytest.Module, 'session':pytest.Session}
6.
7. @classmethod
8. def getManager(cls, item, scope='session'): # 这里修改成 session
如果
1. > pip install pytest-dependency
1. class TestExample(object):
2.
3. @pytest.mark.dependency()
4. def test_a(self):
5. assert False
6.
7. @pytest.mark.dependency()
8. def test_b(self):
9. assert False
10.
11. @pytest.mark.dependency(depends=["TestExample::test_a"])
12. def test_c(self):
13. # TestExample::test_a 没通过则不执行该条用例
14. # 可以跨 Class 筛选
15. print("Hello I am in test_c")
16.
17. @pytest.mark.dependency(depends=["TestExample::test_a","TestExample::test_b"])
18. def test_d(self):
19. print("Hello I am in test_d")
1. pytest -v test_demo.py
2. 2 failed
3. - test_1.py:6 TestExample.test_a
4. - test_1.py:10 TestExample.test_b
5. 2 skipped
Pytest 自定义标记,执行用例筛选作用
1. 使用 @pytest.mark 模块给类或者函数加上标记,用于执行用例时进行筛选
1. @pytest.mark.webtest
2. def test_webtest():
3. pass
4.
5.
6. @pytest.mark.apitest
7. class TestExample(object):
8. def test_a(self):
9. pass
10.
11. @pytest.mark.httptest
12. def test_b(self):
13. pass
仅执行标记 webtest 的用例
1. pytest -v -m webtest
2.
3. Results (0.03s):
4. 1 passed
5. 2 deselected
执行标记多条用例
1. pytest -v -m "webtest or apitest"
2.
3. Results (0.05s):
4. 3 passed
仅不执行标记 webtest 的用例
1. pytest -v -m "not webtest"
2.
3. Results (0.04s):
4. 2 passed
5. 1 deselected
不执行标记多条用例
1. pytest -v -m "not webtest and not apitest"
2.
3. Results (0.02s):
4. 3 deselected
2. 根据 test 节点选择用例
1. pytest -v Test_example.py::TestClass::test_a
2. pytest -v Test_example.py::TestClass
3. pytest -v Test_example.py Test_example2.py
3. 使用 pytest hook 批量标记用例
1. # conftet.py
2.
3. def pytest_collection_modifyitems(items):
4. """
5. 获取每个函数名字,当用例中含有该字符则打上标记
6. """
7. for item in items:
8. if "http" in item.nodeid:
9. item.add_marker(pytest.mark.http)
10. elif "api" in item.nodeid:
11. item.add_marker(pytest.mark.api)
1. class TestExample(object):
2. def test_api_1(self):
3. pass
4.
5. def test_api_2(self):
6. pass
7.
8. def test_http_1(self):
9. pass
10.
11. def test_http_2(self):
12. pass
13. def test_demo(self):
14. pass
仅执行标记 API 的用例
1. pytest -v -m api
2. Results (0.03s):
3. 2 passed
4. 3 deselected
5. 可以看到使用批量标记之后,测试用例中只执行了带有 api 的方法
用例错误处理截图,App 日志等
1. 第一种使用 python 函数装饰器方法
1. def monitorapp(function):
2. """
3. 用例装饰器,截图,日志,是否跳过等
4. 获取系统log,Android logcat、ios 使用syslog
5. """
6.
7. @wraps(function)
8. def wrapper(self, *args, **kwargs):
9. try:
10. allure.dynamic.description('用例开始时间:{}'.format(datetime.datetime.now()))
11. function(self, *args, **kwargs)
12. self.Action.driver.get_log('logcat')
13. except Exception as E:
14. f = self.Action.driver.get_screenshot_as_png()
15. allure.attach(f, '失败截图', allure.attachment_type.PNG)
16. logcat = self.Action.driver.get_log('logcat')
17. c = '\n'.join([i['message'] for i in logcat])
18. allure.attach(c, 'APPlog', allure.attachment_type.TEXT)
19. raise E
20. finally:
21. if self.Action.get_app_pid() != self.Action.Apppid:
22. raise Exception('设备进程 ID 变化,可能发生崩溃')
23. return wrapper
2. 第二种使用 pytest hook 方法 (与方法1二选一)
1. @pytest.hookimpl(tryfirst=True, hookwrapper=True)
2. def pytest_runtest_makereport(item, call):
3. Action = DriverClient().Action
4. outcome = yield
5. rep = outcome.get_result()
6. if rep.when == "call" and rep.failed:
7. f = Action.driver.get_screenshot_as_png()
8. allure.attach(f, '失败截图', allure.attachment_type.PNG)
9. logcat = Action.driver.get_log('logcat')
10. c = '\n'.join([i['message'] for i in logcat])
11. allure.attach(c, 'APPlog', allure.attachment_type.TEXT)
12. if Action.get_app_pid() != Action.apppid:
13. raise Exception('设备进程 ID 变化,可能发生崩溃')
Pytest 另一些 hook 的使用方法
1. 自定义 Pytest 参数
1. > pytest -s -all
1. # content of conftest.py
2. def pytest_addoption(parser):
3. """
4. 自定义参数
5. """
6. parser.addoption("--all", action="store_true",default="type1",help="run all combinations")
7.
8. def pytest_generate_tests(metafunc):
9. if 'param' in metafunc.fixturenames:
10. if metafunc.config.option.all: # 这里能获取到自定义参数
11. paramlist = [1,2,3]
12. else:
13. paramlist = [1,2,4]
14. metafunc.parametrize("param",paramlist) # 给用例加参数化
15.
16. # 怎么在测试用例中获取自定义参数呢
17. # content of conftest.py
18. def pytest_addoption(parser):
19. """
20. 自定义参数
21. """
22. parser.addoption("--cmdopt", action="store_true",default="type1",help="run all combinations")
23.
24.
25. @pytest.fixture
26. def cmdopt(request):
27. return request.config.getoption("--cmdopt")
28.
29.
30. # test_sample.py 测试用例中使用
31. def test_sample(cmdopt):
32. if cmdopt == "type1":
33. print("first")
34. elif cmdopt == "type2":
35. print("second")
36. assert 1
37.
38. > pytest -q --cmdopt=type2
39. second
40. .
41. 1 passed in 0.09 seconds
2. Pytest 过滤测试目录
1. #过滤 pytest 需要执行的文件夹或者文件名字
2. def pytest_ignore_collect(path,config):
3. if 'logcat' in path.dirname:
4. return True #返回 True 则该文件不执行
Pytest 一些常用方法
1. Pytest 用例优先级(比如优先登录什么的)
1. > pip install pytest-ordering
1. @pytest.mark.run(order=1)
2. class TestExample:
3. def test_a(self):
2. Pytest 用例失败重试
1. #原始方法
2. pytet -s test_demo.py
3. pytet -s --lf test_demo.py #第二次执行时,只会执行失败的用例
4. pytet -s --ll test_demo.py #第二次执行时,会执行所有用例,但会优先执行失败用例
5. #使用第三方插件
6. pip install pytest-rerunfailures #使用插件
7. pytest --reruns 2 # 失败case重试两次
3. Pytest 其他常用参数
1. pytest --maxfail=10 #失败超过10次则停止运行
2. pytest -x test_demo.py #出现失败则停止
总结
以上,尽可能的汇总了常见的问题和好用的方法,希望对测试同学们有帮助!下一篇文章将计划讲解 用 Pytest hook 函数运行 yaml 文件来驱动
Appium 做自动化测试实战,并提供测试源码,敬请期待!
**-
来霍格沃兹测试开发学社,学习更多软件测试与测试开发的进阶技术,知识点涵盖web自动化测试 app自动化测试、接口自动化测试、测试框架、性能测试、安全测试、持续集成/持续交付/DevOps,测试左移、测试右移、精准测试、测试平台开发、测试管理等内容,课程技术涵盖bash、pytest、junit、selenium、appium、postman、requests、httprunner、jmeter、jenkins、docker、k8s、elk、sonarqube、jacoco、jvm-sandbox等相关技术,全面提升测试开发工程师的技术实力
QQ交流群:484590337
公众号 TestingStudio
点击获取更多信息
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
2021-01-14 软件测试 | 高质量接口自动化测试必须关注的几个阶段