本文首发于 TesterHome 社区, 文末有福利 !链接 https://testerhome.com/topics/19327
做 UI 自动化测试有段时间了,在 TesterHome
社区看了大量文章,也在网上搜集了不少资料,加上自己写代码、调试过程中摸索了很多东西,踩了不少坑,才有了这篇文章。希望能给做 UI
自动化测试小伙伴们提供些许帮助。
文本主要介绍用 Pytest+Allure+Appium 实现 UI
自动化测试过程中的一些好用的方法和避坑经验。文章可能有点干,看官们多喝水!O(∩_∩)O~
主要用了啥:
1. # Appium 启动时增加 --relaxed-security 参数 Appium 即可执行类似adb shell的方法
2.
3. 2. > appium -p 4723 --relaxed-security
4.
5.
6.
7.
8. 1. # 使用方法
9.
10. 2. def adb_shell(self, command, args, includeStderr=False):
11.
12. 3. """
13.
14. 4. appium --relaxed-security 方式启动
15.
16. 5. adb_shell('ps',['|','grep','android'])
17.
18. 6.
19.
20.
21. 7. :param command:命令
22.
23. 8. :param args:参数
24.
25. 9. :param includeStderr: 为 True 则抛异常
26.
27. 10. :return:
28.
29. 11. """
30.
31. 12. result = self.driver.execute_script('mobile: shell', {
32.
33. 13. 'command': command,
34.
35. 14. 'args': args,
36.
37. 15. 'includeStderr': includeStderr,
38.
39. 16. 'timeout': 5000
40.
41. 17. })
42.
43. 18. return result['stdout']
44.
45.
46.
1. element = self.driver.find_element_by_id('cn.xxxxxx:id/login_sign')
2.
3. 2. pngbyte = element.screenshot_as_png
4.
5. 3. image_data = BytesIO(pngbyte)
6.
7. 4. img = Image.open(image_data)
8.
9. 5. img.save('element.png')
10.
11. 6. # 该方式能直接获取到登录按钮区域的截图
12.
13.
14.
1. # 使用该方法后,手机端 logcat 缓存会清除归零,从新记录
2.
3. 2. # 建议每条用例执行完执行一边清理,遇到错误再保存减少陈余 log 输出
4.
5. 3. # Android
6.
7. 4. logcat = self.driver.get_log('logcat')
8.
9. 5.
10.
11.
12. 6. # iOS 需要安装 brew install libimobiledevice
13.
14. 7. logcat = self.driver.get_log('syslog')
15.
16. 8.
17.
18.
19. 9. # web 获取控制台日志
20.
21. 10. logcat = self.driver.get_log('browser')
22.
23. 11.
24.
25.
26. 12. c = '\n'.join([i['message'] for i in logcat])
27.
28. 13. allure.attach(c, 'APPlog', allure.attachment_type.TEXT)
29.
30. 14. #写入到 allure 测试报告中
31.
32.
33.
1. # 发送文件
2.
3. 2. #Android
4.
5. 3. driver.push_file('/sdcard/element.png', source_path='D:\works\element.png')
6.
7. 4.
8.
9.
10. 5. # 获取手机文件
11.
12. 6. png = driver.pull_file('/sdcard/element.png')
13.
14. 7. with open('element.png', 'wb') as png1:
15.
16. 8. png1.write(base64.b64decode(png))
17.
18. 9.
19.
20.
21. 10. # 获取手机文件夹,导出的是zip文件
22.
23. 11. folder = driver.pull_folder('/sdcard/test')
24.
25. 12. with open('test.zip', 'wb') as folder1:
26.
27. 13. folder1.write(base64.b64decode(folder))
28.
29. 14.
30.
31.
32. 15. # iOS
33.
34. 16. # 需要安装 ifuse
35.
36. 17. # > brew install ifuse 或者 > brew cask install osxfuse 或者 自行搜索安装方式
37.
38. 18.
39.
40.
41. 19. driver.push_file('/Documents/xx/element.png', source_path='D:\works\element.png')
42.
43. 20.
44.
45.
46. 21. # 向 App 沙盒中发送文件
47.
48. 22. # iOS 8.3 之后需要应用开启 UIFileSharingEnabled 权限不然会报错
49.
50. 23. bundleId = 'cn.xxx.xxx' # APP名字
51.
52. 24. driver.push_file('@{bundleId}/Documents/xx/element.png'.format(bundleId=bundleId), source_path='D:\works\element.png')
53.
54.
55.
很多人都使用过 Unitest,先说一下 Pytest 和 Unitest 在 Hook method上的一些区别:
以下是 Pytest
1. class TestExample:
2.
3. 2. def setup(self):
4.
5. 3. print("setup class:TestStuff")
6.
7. 4.
8.
9.
10. 5. def teardown(self):
11.
12. 6. print ("teardown class:TestStuff")
13.
14. 7.
15.
16.
17. 8. def setup_class(cls):
18.
19. 9. print ("setup_class class:%s" % cls.__name__)
20.
21. 10.
22.
23.
24. 11. def teardown_class(cls):
25.
26. 12. print ("teardown_class class:%s" % cls.__name__)
27.
28. 13.
29.
30.
31. 14. def setup_method(self, method):
32.
33. 15. print ("setup_method method:%s" % method.__name__)
34.
35. 16.
36.
37.
38. 17. def teardown_method(self, method):
39.
40. 18. print ("teardown_method method:%s" % method.__name__)
41.
42.
43.
1. @pytest.fixture()
2.
3. 2. def driver_setup(request):
4.
5. 3. request.instance.Action = DriverClient().init_driver('android')
6.
7. 4. def driver_teardown():
8.
9. 5. request.instance.Action.quit()
10.
11. 6. request.addfinalizer(driver_teardown)
12.
13.
14.
1. class Singleton(object):
2.
3. 2. """单例
4.
5. 3. ElementActions 为自己封装操作类"""
6.
7. 4. Action = None
8.
9. 5.
10.
11.
12. 6. def __new__(cls, *args, **kw):
13.
14. 7. if not hasattr(cls, '_instance'):
15.
16. 8. desired_caps={}
17.
18. 9. host = "http://localhost:4723/wd/hub"
19.
20. 10. driver = webdriver.Remote(host, desired_caps)
21.
22. 11. Action = ElementActions(driver, desired_caps)
23.
24. 12. orig = super(Singleton, cls)
25.
26. 13. cls._instance = orig.__new__(cls, *args, **kw)
27.
28. 14. cls._instance.Action = Action
29.
30. 15. return cls._instance
31.
32. 16.
33.
34.
35. 17. class DriverClient(Singleton):
36.
37. 18. pass
38.
39.
40.
测试用例中调用
1. class TestExample:
2.
3. 2. def setup_class(cls):
4.
5. 3. cls.Action = DriverClient().Action
6.
7. 4.
8.
9.
10. 5. def teardown_class(cls):
11.
12. 6. cls.Action.clear()
13.
14. 7.
15.
16.
17. 8.
18.
19.
20. 9. def test_demo(self)
21.
22. 10. self.Action.driver.launch_app()
23.
24. 11. self.Action.set_text('123')
25.
26.
27.
1. class DriverClient():
2.
3. 2.
4.
5.
6. 3. def init_driver(self,device_name):
7.
8. 4. desired_caps={}
9.
10. 5. host = "http://localhost:4723/wd/hub"
11.
12. 6. driver = webdriver.Remote(host, desired_caps)
13.
14. 7. Action = ElementActions(driver, desired_caps)
15.
16. 8. return Action
17.
18. 9.
19.
20.
21. 10.
22.
23.
24. 11.
25.
26.
27. 12. # 该函数需要放置在 conftest.py, pytest 运行时会自动拾取
28.
29. 13. @pytest.fixture()
30.
31. 14. def driver_setup(request):
32.
33. 15. request.instance.Action = DriverClient().init_driver()
34.
35. 16. def driver_teardown():
36.
37. 17. request.instance.Action.clear()
38.
39. 18. request.addfinalizer(driver_teardown)
40.
41.
42.
测试用例中调用
1. #该装饰器会直接引入driver_setup函数
2.
3. 2. @pytest.mark.usefixtures('driver_setup')
4.
5. 3. class TestExample:
6.
7. 4.
8.
9.
10. 5. def test_demo(self):
11.
12. 6. self.Action.driver.launch_app()
13.
14. 7. self.Action.set_text('123')
15.
16.
17.
1. @pytest.mark.parametrize(('kewords'), [(u"小明"), (u"小红"), (u"小白")])
2.
3. 2. def test_kewords(self,kewords):
4.
5. 3. print(kewords)
6.
7. 4.
8.
9.
10. 5. # 多个参数
11.
12. 6. @pytest.mark.parametrize("test_input,expected", [
13.
14. 7. ("3+5", 8),
15.
16. 8. ("2+4", 6),
17.
18. 9. ("6*9", 42),
19.
20. 10. ])
21.
22. 11. def test_eval(test_input, expected):
23.
24. 12. assert eval(test_input) == expected
25.
26.
27.
1. # conftest.py
2.
3. 2. def pytest_generate_tests(metafunc):
4.
5. 3. """
6.
7. 4. 使用 hook 给用例加加上参数
8.
9. 5. metafunc.cls.params 对应类中的 params 参数
10.
11. 6.
12.
13.
14. 7. """
15.
16. 8. try:
17.
18. 9. if metafunc.cls.params and metafunc.function.__name__ in metafunc.cls.params: ## 对应 TestClass params
19.
20. 10. funcarglist = metafunc.cls.params[metafunc.function.__name__]
21.
22. 11. argnames = list(funcarglist[0])
23.
24. 12. metafunc.parametrize(argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist])
25.
26. 13. except AttributeError:
27.
28. 14. pass
29.
30. 15.
31.
32.
33. 16. # test_demo.py
34.
35. 17. class TestClass:
36.
37. 18. """
38.
39. 19. :params 对应 hook 中 metafunc.cls.params
40.
41. 20. """
42.
43. 21. # params = Parameterize('TestClass.yaml').getdata()
44.
45. 22.
46.
47.
48. 23. params = {
49.
50. 24. 'test_a': [{'a': 1, 'b': 2}, {'a': 1, 'b': 2}],
51.
52. 25. 'test_b': [{'a': 1, 'b': 2}, {'a': 1, 'b': 2}],
53.
54. 26. }
55.
56. 27. def test_a(self, a, b):
57.
58. 28. assert a == b
59.
60. 29. def test_b(self, a, b):
61.
62. 30. assert a == b
63.
64.
65.
使用 pytest-dependency 库可以创造依赖关系。
当上层用例没通过,后续依赖关系用例将直接跳过,可以跨 Class 类筛选。如果需要跨 .py
文件运行 需要将 site- packages/pytest_dependency.py
文件的
1. class DependencyManager(object):
2.
3. 2. """Dependency manager, stores the results of tests.
4.
5. 3. """
6.
7. 4.
8.
9.
10. 5. ScopeCls = {'module':pytest.Module, 'session':pytest.Session}
11.
12. 6.
13.
14.
15. 7. @classmethod
16.
17. 8. def getManager(cls, item, scope='session'): # 这里修改成 session
18.
19.
20.
如果
1. > pip install pytest-dependency
2.
3.
4.
5.
6. 1. class TestExample(object):
7.
8. 2.
9.
10.
11. 3. @pytest.mark.dependency()
12.
13. 4. def test_a(self):
14.
15. 5. assert False
16.
17. 6.
18.
19.
20. 7. @pytest.mark.dependency()
21.
22. 8. def test_b(self):
23.
24. 9. assert False
25.
26. 10.
27.
28.
29. 11. @pytest.mark.dependency(depends=["TestExample::test_a"])
30.
31. 12. def test_c(self):
32.
33. 13. # TestExample::test_a 没通过则不执行该条用例
34.
35. 14. # 可以跨 Class 筛选
36.
37. 15. print("Hello I am in test_c")
38.
39. 16.
40.
41.
42. 17. @pytest.mark.dependency(depends=["TestExample::test_a","TestExample::test_b"])
43.
44. 18. def test_d(self):
45.
46. 19. print("Hello I am in test_d")
47.
48.
49.
50.
51. 1. pytest -v test_demo.py
52.
53. 2. 2 failed
54.
55. 3. - test_1.py:6 TestExample.test_a
56.
57. 4. - test_1.py:10 TestExample.test_b
58.
59. 5. 2 skipped
60.
61.
62.
1. @pytest.mark.webtest
2.
3. 2. def test_webtest():
4.
5. 3. pass
6.
7. 4.
8.
9.
10. 5.
11.
12.
13. 6. @pytest.mark.apitest
14.
15. 7. class TestExample(object):
16.
17. 8. def test_a(self):
18.
19. 9. pass
20.
21. 10.
22.
23.
24. 11. @pytest.mark.httptest
25.
26. 12. def test_b(self):
27.
28. 13. pass
29.
30.
31.
仅执行标记 webtest 的用例
1. pytest -v -m webtest
2.
3. 2.
4.
5.
6. 3. Results (0.03s):
7.
8. 4. 1 passed
9.
10. 5. 2 deselected
11.
12.
13.
执行标记多条用例
1. pytest -v -m "webtest or apitest"
2.
3. 2.
4.
5.
6. 3. Results (0.05s):
7.
8. 4. 3 passed
9.
10.
11.
仅不执行标记 webtest 的用例
1. pytest -v -m "not webtest"
2.
3. 2.
4.
5.
6. 3. Results (0.04s):
7.
8. 4. 2 passed
9.
10. 5. 1 deselected
11.
12.
13.
不执行标记多条用例
1. pytest -v -m "not webtest and not apitest"
2.
3. 2.
4.
5.
6. 3. Results (0.02s):
7.
8. 4. 3 deselected
9.
10.
11.
1. pytest -v Test_example.py::TestClass::test_a
2.
3. 2. pytest -v Test_example.py::TestClass
4.
5. 3. pytest -v Test_example.py Test_example2.py
6.
7.
8.
1. # conftet.py
2.
3. 2.
4.
5.
6. 3. def pytest_collection_modifyitems(items):
7.
8. 4. """
9.
10. 5. 获取每个函数名字,当用例中含有该字符则打上标记
11.
12. 6. """
13.
14. 7. for item in items:
15.
16. 8. if "http" in item.nodeid:
17.
18. 9. item.add_marker(pytest.mark.http)
19.
20. 10. elif "api" in item.nodeid:
21.
22. 11. item.add_marker(pytest.mark.api)
23.
24.
25.
26.
27. 1. class TestExample(object):
28.
29. 2. def test_api_1(self):
30.
31. 3. pass
32.
33. 4.
34.
35.
36. 5. def test_api_2(self):
37.
38. 6. pass
39.
40. 7.
41.
42.
43. 8. def test_http_1(self):
44.
45. 9. pass
46.
47. 10.
48.
49.
50. 11. def test_http_2(self):
51.
52. 12. pass
53.
54. 13. def test_demo(self):
55.
56. 14. pass
57.
58.
59.
仅执行标记 API 的用例
1. pytest -v -m api
2.
3. 2. Results (0.03s):
4.
5. 3. 2 passed
6.
7. 4. 3 deselected
8.
9. 5. 可以看到使用批量标记之后,测试用例中只执行了带有 api 的方法
10.
11.
12.
1. def monitorapp(function):
2.
3. 2. """
4.
5. 3. 用例装饰器,截图,日志,是否跳过等
6.
7. 4. 获取系统log,Android logcat、ios 使用syslog
8.
9. 5. """
10.
11. 6.
12.
13.
14. 7. @wraps(function)
15.
16. 8. def wrapper(self, *args, **kwargs):
17.
18. 9. try:
19.
20. 10. allure.dynamic.description('用例开始时间:{}'.format(datetime.datetime.now()))
21.
22. 11. function(self, *args, **kwargs)
23.
24. 12. self.Action.driver.get_log('logcat')
25.
26. 13. except Exception as E:
27.
28. 14. f = self.Action.driver.get_screenshot_as_png()
29.
30. 15. allure.attach(f, '失败截图', allure.attachment_type.PNG)
31.
32. 16. logcat = self.Action.driver.get_log('logcat')
33.
34. 17. c = '\n'.join([i['message'] for i in logcat])
35.
36. 18. allure.attach(c, 'APPlog', allure.attachment_type.TEXT)
37.
38. 19. raise E
39.
40. 20. finally:
41.
42. 21. if self.Action.get_app_pid() != self.Action.Apppid:
43.
44. 22. raise Exception('设备进程 ID 变化,可能发生崩溃')
45.
46. 23. return wrapper
47.
48.
49.
1. @pytest.hookimpl(tryfirst=True, hookwrapper=True)
2.
3. 2. def pytest_runtest_makereport(item, call):
4.
5. 3. Action = DriverClient().Action
6.
7. 4. outcome = yield
8.
9. 5. rep = outcome.get_result()
10.
11. 6. if rep.when == "call" and rep.failed:
12.
13. 7. f = Action.driver.get_screenshot_as_png()
14.
15. 8. allure.attach(f, '失败截图', allure.attachment_type.PNG)
16.
17. 9. logcat = Action.driver.get_log('logcat')
18.
19. 10. c = '\n'.join([i['message'] for i in logcat])
20.
21. 11. allure.attach(c, 'APPlog', allure.attachment_type.TEXT)
22.
23. 12. if Action.get_app_pid() != Action.apppid:
24.
25. 13. raise Exception('设备进程 ID 变化,可能发生崩溃')
26.
27.
28.
1. > pytest -s -all
2.
3.
4.
5.
6. 1. # content of conftest.py
7.
8. 2. def pytest_addoption(parser):
9.
10. 3. """
11.
12. 4. 自定义参数
13.
14. 5. """
15.
16. 6. parser.addoption("--all", action="store_true",default="type1",help="run all combinations")
17.
18. 7.
19.
20.
21. 8. def pytest_generate_tests(metafunc):
22.
23. 9. if 'param' in metafunc.fixturenames:
24.
25. 10. if metafunc.config.option.all: # 这里能获取到自定义参数
26.
27. 11. paramlist = [1,2,3]
28.
29. 12. else:
30.
31. 13. paramlist = [1,2,4]
32.
33. 14. metafunc.parametrize("param",paramlist) # 给用例加参数化
34.
35. 15.
36.
37.
38. 16. # 怎么在测试用例中获取自定义参数呢
39.
40. 17. # content of conftest.py
41.
42. 18. def pytest_addoption(parser):
43.
44. 19. """
45.
46. 20. 自定义参数
47.
48. 21. """
49.
50. 22. parser.addoption("--cmdopt", action="store_true",default="type1",help="run all combinations")
51.
52. 23.
53.
54.
55. 24.
56.
57.
58. 25. @pytest.fixture
59.
60. 26. def cmdopt(request):
61.
62. 27. return request.config.getoption("--cmdopt")
63.
64. 28.
65.
66.
67. 29.
68.
69.
70. 30. # test_sample.py 测试用例中使用
71.
72. 31. def test_sample(cmdopt):
73.
74. 32. if cmdopt == "type1":
75.
76. 33. print("first")
77.
78. 34. elif cmdopt == "type2":
79.
80. 35. print("second")
81.
82. 36. assert 1
83.
84. 37.
85.
86.
87. 38. > pytest -q --cmdopt=type2
88.
89. 39. second
90.
91. 40. .
92.
93. 41. 1 passed in 0.09 seconds
94.
95.
96.
1. #过滤 pytest 需要执行的文件夹或者文件名字
2.
3. 2. def pytest_ignore_collect(path,config):
4.
5. 3. if 'logcat' in path.dirname:
6.
7. 4. return True #返回 True 则该文件不执行
8.
9.
10.
1. > pip install pytest-ordering
2.
3.
4.
5.
6. 1. @pytest.mark.run(order=1)
7.
8. 2. class TestExample:
9.
10. 3. def test_a(self):
11.
12.
13.
1. #原始方法
2.
3. 2. pytet -s test_demo.py
4.
5. 3. pytet -s --lf test_demo.py #第二次执行时,只会执行失败的用例
6.
7. 4. pytet -s --ll test_demo.py #第二次执行时,会执行所有用例,但会优先执行失败用例
8.
9. 5. #使用第三方插件
10.
11. 6. pip install pytest-rerunfailures #使用插件
12.
13. 7. pytest --reruns 2 # 失败case重试两次
14.
15.
16.
1. pytest --maxfail=10 #失败超过10次则停止运行
2.
3. 2. pytest -x test_demo.py #出现失败则停止
4.
5.
6.
以上,尽可能的汇总了常见的问题和好用的方法,希望对测试同学们有帮助!下一篇文章将计划讲解 用 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
视频资料领取:https://qrcode.testing-studio.com/f?from=CSDN&url=https://ceshiren.com/t/topic/15844
点击查看更多信息