霍格沃兹测试开发学社

《Python测试开发进阶训练营》(随到随学!)
2023年第2期《Python全栈开发与自动化测试班》(开班在即)
报名联系weixin/qq:2314507862

用 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
点击获取更多信息

posted @ 2022-01-14 09:14  霍格沃兹测试开发学社  阅读(223)  评论(0编辑  收藏  举报