• pytest框架二次开发


    目录

    一、背景:

    二、实现思路:

    2.1 报警接口

    2.2 、HOOK函数:

    2.2.1 pytest_runtest_makereport

    2.2.2 pytest_collectstart

    2.2.3 pytest_collectreport

    三、项目实战:

    3.1 我们先实现报警工具

    3.2 跨模块的全局变量

    3.3 通过HOOK函数收集报警信息&报警


    一、背景:

    我想要实现的效果,当接口自动化case运行失败时,触发企业微信机器人报警,艾特相关人员,及发送失败case的相关信息。

    报警信息包括:case等级、case描述、case名称、case的开发人员。

    二、实现思路:

    2.1 报警接口

     实现报警接口。七、钉钉机器人报警设置_傲娇的喵酱的博客-CSDN博客_钉钉报警机器人

    我们要通过企业微信实现。企业微信群聊机器人代码示例分享 - 开发者社区 - 企业微信开发者中心

     可以去查看官方文档。

    case运行时,我们可以对报警做一个筛选,哪些级别的case报警,哪些级别的case不报警。或者可以再扩展一下其他逻辑。

    模版:

    1. requests.post('https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的机器人的key',
    2. headers={'Content-Type': 'application/json'},data=json.dumps({'msgtype': 'text','text':{'mentioned_mobile_list': '你要艾特的人手机号列表','content': '{}'.format(msg).encode('utf-8').decode(
    3. "latin1")}}, ensure_ascii=False))

    注意:

    1、艾特人,可以通过uid,也可以通过手机号,我们这里通过手机号 'mentioned_mobile_list',

    是一个list,如

    'mentioned_mobile_list':["1871871817817",]

    2、 内容为中文的话,需要注意编码。

    .encode('utf-8').decode("latin1")


     

    2.2 、HOOK函数:

    我们需要收集一共执行了多少case、失败了多少case、每条case的用例级别、用例描述、作者、失败原因等。

    实现这些数据的收集,我们需要使用pytest 提供的Hook函数。

    官方文档:

    Writing hook functions — pytest documentation

    API Reference — pytest documentation

    HOOK函数:

    1、是个函数,在系统消息触发时别系统调用

    2、不是用户自己触发的

    3、使用时直接编写函数体

    4、钩子函数的名称是确定,当系统消息触发,自动会调用

    HOOK也被称作钩子。他是系统或者第三方插件暴露的一种可被回调的函数

    pytest具体提供了哪些hook函数,可以在\venv\Lib\site-packages\_pytest>hookspec.py文件中查看,里面每一个钩子函数都有相应的介绍。

    插件就是用1个或者多个hook函数。如果想要编写新的插件,或者是仅仅改进现有的插件,都必须通过这个hook函数进行。所以想掌握pytest插件二次开发,必须搞定hook函数。

    Hook函数都在这个路径下:site-packages/_pytest/hookspec.py

    我们看一下hookspec.py源码,看一看pytest这个框架都给我们提供了哪些回调函数。

    2.2.1 pytest_runtest_makereport

    pytest框架,提供了pytest_runtest_makereport回调函数,来获取用例的执行结果。

    官方文档:

    API Reference — pytest documentation

    1. @hookspec(firstresult=True)
    2. def pytest_runtest_makereport(
    3. item: "Item", call: "CallInfo[None]"
    4. ) -> Optional["TestReport"]:
    5. """Called to create a :py:class:`_pytest.reports.TestReport` for each of
    6. the setup, call and teardown runtest phases of a test item.
    7. See :func:`pytest_runtest_protocol` for a description of the runtest protocol.
    8. :param CallInfo[None] call: The ``CallInfo`` for the phase.
    9. Stops at first non-None result, see :ref:`firstresult`.
    10. """

    (python好像没有抽象类与接口),这里框架提供了一个pytest_runtest_makereport函数模版,我们需要去实现它,实现它的内部逻辑。

    函数名称:pytest_runtest_makereport

    传参:

    ---Item类的实例item,测试用例。

    ---CallInfo[None]类的实例call

    返回值:Optional["TestReport"]

    TestReport类的实例,或者为None(Optional更优雅的处理none)

    备注:

    Optional介绍:

    Python:Optional和带默认值的参数_秋墓的博客-CSDN博客_optional python

    每个case的执行过程,其实都是分三步的,1 setup初始化数据 2执行case 3teardown

    这里的item是测试用例,call是测试步骤,具体执行过程如下:

    *先执行when="setup"返回setup的执行结果

    *再执行when="call"返回call的执行结果

    *最后执行when="teardown"返回teardown的执行结果。

    查看一下返回结果:TestReport源码

    1. @final
    2. class TestReport(BaseReport):
    3. """Basic test report object (also used for setup and teardown calls if
    4. they fail)."""
    5. __test__ = False
    6. def __init__(
    7. self,
    8. nodeid: str,
    9. location: Tuple[str, Optional[int], str],
    10. keywords,
    11. outcome: "Literal['passed', 'failed', 'skipped']",
    12. longrepr: Union[
    13. None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
    14. ],
    15. when: "Literal['setup', 'call', 'teardown']",
    16. sections: Iterable[Tuple[str, str]] = (),
    17. duration: float = 0,
    18. user_properties: Optional[Iterable[Tuple[str, object]]] = None,
    19. **extra,
    20. ) -> None:
    21. #: Normalized collection nodeid.
    22. self.nodeid = nodeid
     
    

    这里有用的信息比较多:

    outcome 执行结果:passed or failed

    nodeid case名称

    longrepr 执行失败原因

    item.function.__doc__ case描述信息

     
    
     
    

    举🌰:

    conftest.py

    1. import pytest
    2. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
    3. def pytest_runtest_makereport(item, call):
    4. print('-------------------------------')
    5. # 获取常规的钩子方法的调用结果,返回一个result对象
    6. out = yield
    7. print('用例的执行结果', out)
    8. # 获取调用结果的测试报告,返回一个report对象,report对象的属性
    9. # 包括when(setup, call, teardown三个值)、nodeid(测试用例的名字)、
    10. # outcome(用例执行的结果 passed, failed)
    11. report = out.get_result()
    12. print('测试报告: %s' % report)
    13. print('步骤:%s' % report.when)
    14. print('nodeid: %s' % report.nodeid)
    15. # 打印函数注释信息
    16. print('description: %s' % str(item.function.__doc__))
    17. print('运行结果: %s' % report.outcome)

    test_cs1.py

    1. import pytest
    2. def setup_function():
    3. print(u"setup_function:每个用例开始前都会执行")
    4. def teardown_function():
    5. print(u"teardown_function:每个用例结束后都会执行")
    6. def test_01():
    7. """ 用例描述:用例1"""
    8. print("用例1-------")
    9. def test_02():
    10. """ 用例描述:用例2"""
    11. print("用例2------")
    12. if __name__ == '__main__':
    13. pytest.main(['-s'])

    运行结果:

    1. ============================= test session starts ==============================
    2. collecting ... collected 2 items
    3. test_cs1.py::test_01 -------------------------------
    4. 用例的执行结果 object at 0x7ffb48478090>
    5. 测试报告: 'test_cs1.py::test_01' when='setup' outcome='passed'>
    6. 步骤:setup
    7. nodeid: test_cs1.py::test_01
    8. description: 用例描述:用例1
    9. 运行结果: passed
    10. setup_function:每个用例开始前都会执行
    11. -------------------------------
    12. 用例的执行结果 object at 0x7ffb68786b90>
    13. 测试报告: 'test_cs1.py::test_01' when='call' outcome='passed'>
    14. 步骤:call
    15. nodeid: test_cs1.py::test_01
    16. description: 用例描述:用例1
    17. 运行结果: passed
    18. PASSED [ 50%]用例1-------
    19. -------------------------------
    20. 用例的执行结果 object at 0x7ffb48478090>
    21. 测试报告: 'test_cs1.py::test_01' when='teardown' outcome='passed'>
    22. 步骤:teardown
    23. nodeid: test_cs1.py::test_01
    24. description: 用例描述:用例1
    25. 运行结果: passed
    26. teardown_function:每个用例结束后都会执行
    27. test_cs1.py::test_02 -------------------------------
    28. 用例的执行结果 object at 0x7ffb584ef710>
    29. 测试报告: 'test_cs1.py::test_02' when='setup' outcome='passed'>
    30. 步骤:setup
    31. nodeid: test_cs1.py::test_02
    32. description: 用例描述:用例2
    33. 运行结果: passed
    34. setup_function:每个用例开始前都会执行
    35. -------------------------------
    36. 用例的执行结果 object at 0x7ffb584ef710>
    37. 测试报告: 'test_cs1.py::test_02' when='call' outcome='passed'>
    38. 步骤:call
    39. nodeid: test_cs1.py::test_02
    40. description: 用例描述:用例2
    41. 运行结果: passed
    42. PASSED [100%]用例2------
    43. -------------------------------
    44. 用例的执行结果 object at 0x7ffb6876e390>
    45. 测试报告: 'test_cs1.py::test_02' when='teardown' outcome='passed'>
    46. 步骤:teardown
    47. nodeid: test_cs1.py::test_02
    48. description: 用例描述:用例2
    49. 运行结果: passed
    50. teardown_function:每个用例结束后都会执行
    51. ============================== 2 passed in 0.42s ===============================
    52. Process finished with exit code 0

    我们在conftest.py中实现pytest_runtest_makereport这个方法,系统在调用这个函数时,会自动注入传参。

    2.2.2 pytest_collectstart

    pytest框架,提供了pytest_collectstart 开始收集,收集每个模块的case信息

    源码:

    1. def pytest_collectstart(collector: "Collector") -> None:
    2. """Collector starts collecting."""

    我们看一下传参collector: "Collector"源码

    1. class Collector(Node):
    2. """Collector instances create children through collect() and thus
    3. iteratively build a tree."""
    4. class CollectError(Exception):
    5. """An error during collection, contains a custom message."""
    6. def collect(self) -> Iterable[Union["Item", "Collector"]]:
    7. """Return a list of children (items and collectors) for this
    8. collection node."""
    9. raise NotImplementedError("abstract")
    10. # TODO: This omits the style= parameter which breaks Liskov Substitution.
    11. def repr_failure( # type: ignore[override]
    12. self, excinfo: ExceptionInfo[BaseException]
    13. ) -> Union[str, TerminalRepr]:
    14. """Return a representation of a collection failure.
    15. :param excinfo: Exception information for the failure.
    16. """
    17. if isinstance(excinfo.value, self.CollectError) and not self.config.getoption(
    18. "fulltrace", False
    19. ):
    20. exc = excinfo.value
    21. return str(exc.args[0])
    22. # Respect explicit tbstyle option, but default to "short"
    23. # (_repr_failure_py uses "long" with "fulltrace" option always).
    24. tbstyle = self.config.getoption("tbstyle", "auto")
    25. if tbstyle == "auto":
    26. tbstyle = "short"
    27. return self._repr_failure_py(excinfo, style=tbstyle)
    28. def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
    29. if hasattr(self, "fspath"):
    30. traceback = excinfo.traceback
    31. ntraceback = traceback.cut(path=self.fspath)
    32. if ntraceback == traceback:
    33. ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
    34. excinfo.traceback = ntraceback.filter()
    35. def _check_initialpaths_for_relpath(session, fspath):
    36. for initial_path in session._initialpaths:
    37. if fspath.common(initial_path) == initial_path:
    38. return fspath.relto(initial_path)

    类Collector继承了Node类。我们查看一下Node源码。

    1. class Node(metaclass=NodeMeta):
    2. """Base class for Collector and Item, the components of the test
    3. collection tree.
    4. Collector subclasses have children; Items are leaf nodes.
    5. """
    6. # Use __slots__ to make attribute access faster.
    7. # Note that __dict__ is still available.
    8. __slots__ = (
    9. "name",
    10. "parent",
    11. "config",
    12. "session",
    13. "fspath",
    14. "_nodeid",
    15. "_store",
    16. "__dict__",
    17. )
    18. def __init__(
    19. self,
    20. name: str,
    21. parent: "Optional[Node]" = None,
    22. config: Optional[Config] = None,
    23. session: "Optional[Session]" = None,
    24. fspath: Optional[py.path.local] = None,
    25. nodeid: Optional[str] = None,
    26. ) -> None:
    27. #: A unique name within the scope of the parent node.
    28. self.name = name
    29. #: The parent collector node.
    30. self.parent = parent

    属性包括

    1. "name",
    2. "parent",
    3. "config",
    4. "session",
    5. "fspath",
    6. "_nodeid",
    7. "_store",
    8. "__dict__",

    我们在 conftest.py文件下,打印一下这个几个属性。

    1. def pytest_collectstart(collector:Collector):
    2. print("开始用例收集")
    3. print("集合名称:%s"%collector.name)
    4. print("parent",collector.parent)
    5. print("config",collector.config)
    6. print("session",collector.session)
    7. print("fspath",collector.fspath)
    8. print("_nodeid",collector._nodeid)
    9. print("_store", collector._store)
    10. print("_dict__", collector.__dict__)
    11. print("...........................")

    输出结果:

    1. collecting ... 开始用例收集
    2. 集合名称:ChenShuaiTest
    3. parent None
    4. config <_pytest.config.Config object at 0x7fc7a8244e50>
    5. session 0> testsfailed=0 testscollected=0>
    6. fspath /Users/zhaohui/PycharmProjects/ChenShuaiTest
    7. _nodeid
    8. _store <_pytest.store.Store object at 0x7fc7b8993ad0>
    9. _dict__ {'keywords': for node 0> testsfailed=0 testscollected=0>>, 'own_markers': [], 'extra_keyword_matches': set(), 'testsfailed': 0, 'testscollected': 0, 'shouldstop': False, 'shouldfail': False, 'trace': object at 0x7fc7c83bc210>, 'startdir': local('/Users/zhaohui/PycharmProjects/ChenShuaiTest/test_case/mytest'), '_initialpaths': frozenset({local('/Users/zhaohui/PycharmProjects/ChenShuaiTest/test_case/mytest/test_cs1.py')}), '_bestrelpathcache': _bestrelpath_cache(path=PosixPath('/Users/zhaohui/PycharmProjects/ChenShuaiTest')), 'exitstatus': 0>, '_fixturemanager': <_pytest.fixtures.FixtureManager object at 0x7fc7b89936d0>, '_setupstate': <_pytest.runner.SetupState object at 0x7fc7c8502bd0>, '_notfound': [], '_initial_parts': [(local('/Users/zhaohui/PycharmProjects/ChenShuaiTest/test_case/mytest/test_cs1.py'), [])], 'items': []}
    10. ...........................
    11. 收集的用例个数---------------------:1
    12. []
    13. 开始用例收集
    14. 集合名称:test_cs1.py
    15. parent
    16. config <_pytest.config.Config object at 0x7fc7a8244e50>
    17. session 0> testsfailed=0 testscollected=0>
    18. fspath /Users/zhaohui/PycharmProjects/ChenShuaiTest/test_case/mytest/test_cs1.py
    19. _nodeid test_case/mytest/test_cs1.py
    20. _store <_pytest.store.Store object at 0x7fc7a83088d0>
    21. _dict__ {'keywords': for node >, 'own_markers': [], 'extra_keyword_matches': set()}
    22. ...........................
    23. 收集的用例个数---------------------:8
    24. [, , , , , , , ]
    25. collected 8 items

    这里主要展示的就是收集时每个模块的case信息。感觉用处不是很大。

    2.2.3 pytest_collectreport

    pytest框架,提供了pytest_collectreport回调函数,收集完成后case的报告

    源码:

    1. def pytest_collectreport(report: "CollectReport") -> None:
    2. """Collector finished collecting."""

    传参CollectReport 

    1. @final
    2. class CollectReport(BaseReport):
    3. """Collection report object."""
    4. when = "collect"
    5. def __init__(
    6. self,
    7. nodeid: str,
    8. outcome: "Literal['passed', 'skipped', 'failed']",
    9. longrepr,
    10. result: Optional[List[Union[Item, Collector]]],
    11. sections: Iterable[Tuple[str, str]] = (),
    12. **extra,
    13. ) -> None:
    14. #: Normalized collection nodeid.
    15. self.nodeid = nodeid
    16. #: Test outcome, always one of "passed", "failed", "skipped".
    17. self.outcome = outcome
    18. #: None or a failure representation.
    19. self.longrepr = longrepr
    20. #: The collected items and collection nodes.
    21. self.result = result or []
    22. #: List of pairs ``(str, str)`` of extra information which needs to
    23. #: marshallable.
    24. # Used by pytest to add captured text : from ``stdout`` and ``stderr``,
    25. # but may be used by other plugins : to add arbitrary information to
    26. # reports.
    27. self.sections = list(sections)
    28. self.__dict__.update(extra)
    29. @property
    30. def location(self):
    31. return (self.fspath, None, self.fspath)
    32. def __repr__(self) -> str:
    33. return "".format(
    34. self.nodeid, len(self.result), self.outcome
    35. )

    类CollectReport继承了

    属性:

    nodeid: str,
    outcome: "Literal['passed', 'skipped', 'failed']",
    longrepr,
    result: Optional[List[Union[Item, Collector]]],
    sections: Iterable[Tuple[str, str]] = (),

    我们把这些属性打印一下:

    1. def pytest_collectreport(report: CollectReport):
    2. print("收集的用例个数---------------------:%s"%len(report.result))
    3. print("result",report.result)
    4. print("nodeid", report.nodeid)
    5. print("outcome", report.outcome)
    6. print("result", report.result)
    7. print("sections", report.sections)
    1. collecting ... 收集的用例个数---------------------:1
    2. result []
    3. nodeid
    4. outcome passed
    5. result []
    6. sections []
    7. 收集的用例个数---------------------:8
    8. result [, , , , , , , ]
    9. nodeid test_case/mytest/test_cs1.py
    10. outcome passed
    11. result [, , , , , , , ]
    12. sections []
    13. collected 8 items

    感觉也没啥太有用的东西.... 

    三、项目实战:

    3.1 我们先实现报警工具

    alarm_utils.py 下,AlarmUtils工具类

    1. # -*- coding:utf-8 -*-
    2. # @Author: 喵酱
    3. # @time: 2022 - 09 -15
    4. # @File: alarm_utils.py
    5. import requests
    6. import json
    7. class AlarmUtils:
    8. @staticmethod
    9. def default_alram(alarm_content:str,phone_list:list):
    10. requests.post('https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的机器人的key',
    11. headers={'Content-Type': 'application/json'},
    12. data=json.dumps(
    13. {'msgtype': 'text', 'text': {'mentioned_mobile_list':phone_list,
    14. 'content': '{}'.format(alarm_content).encode('utf-8').decode(
    15. "latin1")}},
    16. ensure_ascii=False))
    这里艾特人的方式,通过手机号,是一个list。这个方法需要传报警内容及艾特的手机号。

    3.2 跨模块的全局变量

    Python设置跨文件的全局变量_he2693836的博客-CSDN博客_python跨文件全局变量

    1. # -*- coding:utf-8 -*-
    2. # @Author: 喵酱
    3. # @time: 2022 - 09 -14
    4. # @File: gol.py
    5. # -*- coding: utf-8 -*-
    6. def _init(): # 初始化
    7. global _global_dict
    8. _global_dict = {}
    9. def set_value(key, value):
    10. # 定义一个全局变量
    11. _global_dict[key] = value
    12. def get_value(key):
    13. # 获得一个全局变量,不存在则提示读取对应变量失败
    14. try:
    15. return _global_dict[key]
    16. except:
    17. print('读取' + key + '失败\r\n')

    3.3 通过HOOK函数收集报警信息&报警

    conftest.py

    1. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
    2. def pytest_runtest_makereport(item, call)-> Optional[TestReport]:
    3. print('-------------------------------')
    4. # 获取常规的钩子方法的调用结果,返回一个result对象
    5. out = yield
    6. # '用例的执行结果', out
    7. # 获取调用结果的测试报告,返回一个report对象,report对象的属性
    8. # 包括when(setup, call, teardown三个值)、nodeid(测试用例的名字)、
    9. # outcome(用例执行的结果 passed, failed)
    10. report = out.get_result()
    11. # 只关注用例本身结果
    12. if report.when == "call":
    13. num = gol.get_value("total")
    14. gol.set_value("total",num+1)
    15. if report.outcome != "passed":
    16. failnum = gol.get_value("fail_num")
    17. gol.set_value("fail_num", failnum + 1)
    18. single_fail_content = "{}.报错case名称:{},{},失败原因:{}".format(gol.get_value("fail_num"), report.nodeid,
    19. str(item.function.__doc__),report.longrepr)
    20. list_content:list = gol.get_value("fail_content")
    21. list_content.append(single_fail_content)
    22. @pytest.fixture(scope="session", autouse=True)
    23. def fix_a():
    24. gol._init()
    25. gol.set_value("total",0)
    26. gol.set_value("fail_num", 0)
    27. # 失败内容
    28. gol.set_value("fail_content", [])
    29. yield
    30. # 执行case总数
    31. all_num = str(gol.get_value("total"))
    32. # 失败case总数:
    33. fail_num = str(gol.get_value("fail_num"))
    34. if int(gol.get_value("fail_num")):
    35. list_content: list = gol.get_value("fail_content")
    36. final_alarm_content = "项目名称:{},\n执行case总数:{},失败case总数:{},\n详情:{}".format("陈帅的测试项目", all_num, fail_num,
    37. str(list_content))
    38. print(final_alarm_content )
    39. AlarmUtils.default_alram(final_alarm_content, ['1871xxxxxx'])

    大概是这个样子了,后续一些细节慢慢优化吧

    pytest进行二次开发,主要依赖于Hook函数。在conftest.py文件里调用Hook函数。

    conftest.py是pytest特有的本地测试配置文件,既可以用来设置项目级的fixture,也可以用来导入外部插件,还可以指定钩子函数。

    conftest.py文件名称是固定的,pytest会自动设别该文件,只作用在它的目录以及子目录。

    通过装饰器@pytest.fixture来告诉pytest某个特定的函数是一个fixture,然后用例可以直接把fixture当参数来调用

    这里的前置后置@pytest.fixture(scope="session", autouse=True),是项目级的。只运行一次。

    下一章:

    pytest框架二次开发之自定义注解_傲娇的喵酱的博客-CSDN博客


     

  • 相关阅读:
    Map集合保存数据库
    MySQL表关联
    傅里叶级数在不连续点会怎么样???
    mybatis复习
    mysql分组concat() concat_ws() group_concat()
    浅谈STL|STL函数对象篇
    MPEG-Pemetrexed 甲氧基聚乙二醇-培美曲塞
    【PCL-11】提取平面上层的目标物,剔除平面下层目标物
    如果你是独立开发者,你是先写前端还是先写后端?
    JavaScript 数值 Number
  • 原文地址:https://blog.csdn.net/qq_39208536/article/details/126764409