• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

用 Pytest+Appium+Allure 做 UI 自动化测试的那些事儿

武飞扬头像
测试界的吴彦祖
帮助1

学新通

做 UI 自动化测试有段时间了,在 TesterHome 社区看了大量文章,也在网上搜集了不少资料,加上自己写代码、调试过程中摸索了很多东西,踩了不少坑,才有了这篇文章。希望能给做 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
  3.  
     
  4.  
     
  5.  
    # 使用方法
  6.  
    def adb_shell(self, command, args, includeStderr=False):
  7.  
    """
  8.  
    appium --relaxed-security 方式启动
  9.  
    adb_shell('ps',['|','grep','android'])
  10.  
     
  11.  
    :param command:命令
  12.  
    :param args:参数
  13.  
    :param includeStderr: 为 True 则抛异常
  14.  
    :return:
  15.  
    """
  16.  
    result = self.driver.execute_script('mobile: shell', {
  17.  
    'command': command,
  18.  
    'args': args,
  19.  
    'includeStderr': includeStderr,
  20.  
    'timeout': 5000
  21.  
    })
  22.  
    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
  2.  
     
  3.  
     
  4.  
    class TestExample(object):
  5.  
     
  6.  
    @pytest.mark.dependency()
  7.  
    def test_a(self):
  8.  
    assert False
  9.  
     
  10.  
    @pytest.mark.dependency()
  11.  
    def test_b(self):
  12.  
    assert False
  13.  
     
  14.  
    @pytest.mark.dependency(depends=["TestExample::test_a"])
  15.  
    def test_c(self):
  16.  
    # TestExample::test_a 没通过则不执行该条用例
  17.  
    # 可以跨 Class 筛选
  18.  
    print("Hello I am in test_c")
  19.  
     
  20.  
    @pytest.mark.dependency(depends=["TestExample::test_a","TestExample::test_b"])
  21.  
    def test_d(self):
  22.  
    print("Hello I am in test_d")
  23.  
     
  24.  
     
  25.  
    pytest -v test_demo.py
  26.  
    2 failed
  27.  
    - test_1.py:6 TestExample.test_a
  28.  
    - test_1.py:10 TestExample.test_b
  29.  
    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)
  12.  
     
  13.  
     
  14.  
    class TestExample(object):
  15.  
    def test_api_1(self):
  16.  
    pass
  17.  
     
  18.  
    def test_api_2(self):
  19.  
    pass
  20.  
     
  21.  
    def test_http_1(self):
  22.  
    pass
  23.  
     
  24.  
    def test_http_2(self):
  25.  
    pass
  26.  
    def test_demo(self):
  27.  
    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
  2.  
     
  3.  
     
  4.  
    # content of conftest.py
  5.  
    def pytest_addoption(parser):
  6.  
    """
  7.  
    自定义参数
  8.  
    """
  9.  
    parser.addoption("--all", action="store_true",default="type1",help="run all combinations")
  10.  
     
  11.  
    def pytest_generate_tests(metafunc):
  12.  
    if 'param' in metafunc.fixturenames:
  13.  
    if metafunc.config.option.all: # 这里能获取到自定义参数
  14.  
    paramlist = [1,2,3]
  15.  
    else:
  16.  
    paramlist = [1,2,4]
  17.  
    metafunc.parametrize("param",paramlist) # 给用例加参数化
  18.  
     
  19.  
    # 怎么在测试用例中获取自定义参数呢
  20.  
    # content of conftest.py
  21.  
    def pytest_addoption(parser):
  22.  
    """
  23.  
    自定义参数
  24.  
    """
  25.  
    parser.addoption("--cmdopt", action="store_true",default="type1",help="run all combinations")
  26.  
     
  27.  
     
  28.  
    @pytest.fixture
  29.  
    def cmdopt(request):
  30.  
    return request.config.getoption("--cmdopt")
  31.  
     
  32.  
     
  33.  
    # test_sample.py 测试用例中使用
  34.  
    def test_sample(cmdopt):
  35.  
    if cmdopt == "type1":
  36.  
    print("first")
  37.  
    elif cmdopt == "type2":
  38.  
    print("second")
  39.  
    assert 1
  40.  
     
  41.  
    > pytest -q --cmdopt=type2
  42.  
    second
  43.  
    .
  44.  
    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
  2.  
     
  3.  
     
  4.  
    @pytest.mark.run(order=1)
  5.  
    class TestExample:
  6.  
    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 做自动化测试实战,并提供测试源码,敬请期待!

福利福利

如果你还有许多困惑,那么我整理的视频资源和文档会是你的良师益友,或许可以给你带来一些实际性的帮助与突破【保证100%免费】

学新通

这些资料,对于在从事【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!

最后: 可以在我的VX公众号:【程序员小濠】 免费领取一份216页软件测试工程师面试宝典文档资料。以及相对应的视频学习教程免费分享!,其中包括了有基础知识、Linux必备、Shell、互联网程序原理、Mysql数据库、抓包工具专题、接口测试工具、测试进阶-Python编程、Web自动化测试、APP自动化测试、接口自动化测试、测试高级持续集成、测试架构开发测试框架、性能测试、安全测试等。也可以和我交流互动哈

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgcgebb
系列文章
更多 icon
同类精品
更多 icon
继续加载