欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

2、pytest中文文档--使用和调用

程序员文章站 2022-07-09 20:39:46
pytest有多种使用和调用形式,很多功能在通常场景中并不会使用,本文可以让你有个大概的了解,标题标粗的章节是我个人认为比较重要的。 ......

使用和调用

通过python -m pytest调用pytest

你可以通过python的解释器来执行测试:

python -m pytest [...]

但是,这和直接执行pytest [...]命令的效果几乎是一模一样的;

pytest执行结束时返回的状态码

pytest命令执行结束,可能会返回以下六种状态码:

  • 0(ok):所有收集到的用例测试通过
  • 1(tests_failed):有用例测试失败
  • 2(interrupted):用户打断测试执行
  • 3(internal_error):测试执行的过程中,发生内部错误
  • 4(usage_error):pytest命令使用错误
  • 5(no_tests_collected):没有收集到测试用例

它们在枚举类_pytest.main.exitcode中声明。并且,其作为公开api的一部分,能够直接引入和访问。

from pytest import exitcode

获取帮助信息

pytest --version  # 查看版本号和pytest的引入路径
pytest -h  # 查看帮助信息

最多允许失败的测试用例数

当达到最大上限时,退出执行;如未配置,则没有上限;

pytest -x  # 遇到第一个失败时,退出执行
pytest --maxfail==2  # 遇到第二个失败时,退出执行

执行指定的测试用例

pytest支持多种方式来执行特定的测试用例:

执行指定模块中的测试用例:

pytest test_mod.py

执行指定目录下的测试用例:

pytest testing/

执行文件名、类名或者函数名中包含特定关键字的测试用例:

yaomengdemacbook-air:src yaomeng$ pytest -k "_class and not two"
============================================================ test session starts =============================================================
platform darwin -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: /users/yaomeng/private/projects/pytest-chinese-doc/src
plugins: repeat-0.8.0
collected 5 items / 4 deselected / 1 selected                                                                                                

test_class.py .                                                                                                                        [100%]

====================================================== 1 passed, 4 deselected in 0.03s =======================================================

执行src/目录下,名字包含_class但不包含two的测试用例,即执行test_class.py模块中的test_one用例。

注意:python的关键字不可以应用在-k选项中,例如,classdef等。

执行指定nodeid的测试用例:

pytest为每一个收集到的测试用例指定一个唯一的nodeid。其由模块名加说明符构成,中间以::间隔。其中,说明符可以是类名、函数名以及由parametrize标记赋予的参数;

# test_nodeid.py

import pytest


def test_one():
    print('test_one')
    assert 1


class testnodeid:
    def test_one(self):
        print('testnodeid::test_one')
        assert 1

    @pytest.mark.parametrize('x,y', [(1, 1), (3, 4)])
    def test_two(self, x, y):
        print(f'testnodeid::test_two::{x} == {y}')
        assert x == y

执行test_one用例:

/d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest -s test_nodeid.py::test_one
================================================= test session starts =================================================
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: d:\personal files\python\pytest-chinese-doc\src
collected 1 item

test_nodeid.py test_one
.

================================================== 1 passed in 0.02s ==================================================

执行testnodeid中的test_one用例:

/d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest -s test_nodeid.py::testnodeid::test_one
================================================= test session starts =================================================
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: d:\personal files\python\pytest-chinese-doc\src
collected 1 item

test_nodeid.py testnodeid::test_one
.

================================================== 1 passed in 0.02s ==================================================

执行testnodeid中的test_two用例,并指定参数xy的值:

λ pytest -s test_nodeid.py::testnodeid::test_two[3-4]
================================================= test session starts =================================================
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: d:\personal files\python\pytest-chinese-doc\src
collected 1 item

test_nodeid.py testnodeid::test_two::3 == 4
f

====================================================== failures ======================================================= ______________________________________________ testnodeid.test_two[3-4] _______________________________________________

self = <test_nodeid.testnodeid object at 0x00000152613c49b0>, x = 3, y = 4

    @pytest.mark.parametrize('x,y', [(1, 1), (3, 4)])
    def test_two(self, x, y):
        print(f'testnodeid::test_two::{x} == {y}')
>       assert x == y
e       assert 3 == 4

test_nodeid.py:37: assertionerror
================================================== 1 failed in 0.05s ==================================================

这里对参数xy赋值的形式是[3-4],中间以-间隔;单个或多个参数的赋值形式以此类比;并且,只能为[1-1]或者[3-4],其它的会报错;

执行指定标记的用例

pytest -m slow

执行所有用@pytest.mark.slow装饰器标记的测试用例。

执行指定包中的测试用例

/d/personal files/python/pytest-chinese-doc (5.1.2)
λ pytest --pyargs src

执行src包中所有的测试用例。

注意:src文件夹中要有一个__init__.py文件。

修改回溯信息的输出模式

pytest回溯信息的输出一共有六种模式:auto/long/short/line/native/no,用--tb选项指定;

pytest -l, --showlocals         # 打印本地变量
pytest --tb=auto                # 默认模式
pytest --tb=long                # 尽可能详细的输出
pytest --tb=short               # 更简短的输出
pytest --tb=line                # 每个失败信息总结在一行中
pytest --tb=native              # python的标准输出
pytest --tb=no                  # 不打印失败信息

--full-trace是一种比--tb=long更详细的输出模式。它甚至能观察到用户打断执行(ctrl+c)时的回溯信息,而上述六种模式默认是不输出信息的。

总结报告

-r选项可以在执行结束后,打印一个简短的总结报告。在执行的测试用例很多时,可以让你对结果有个清晰的了解:

# test_report.py

import pytest


@pytest.fixture
def error_fixture():
    assert 0


def test_ok():
    print("ok")


def test_fail():
    assert 0


def test_error(error_fixture):
    pass


def test_skip():
    pytest.skip("skipping this test")


def test_xfail():
    pytest.xfail("xfailing this test")


@pytest.mark.xfail(reason="always xfail")
def test_xpass():
    pass
/d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest -ra test_report.py
================================================= test session starts =================================================
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: d:\personal files\python\pytest-chinese-doc\src
collected 6 items

test_report.py .fesxx                                                                                            [100%]

======================================================= errors ======================================================== ____________________________________________ error at setup of test_error _____________________________________________

    @pytest.fixture
    def error_fixture():
>       assert 0
e       assert 0

test_report.py:27: assertionerror
====================================================== failures ======================================================= ______________________________________________________ test_fail ______________________________________________________

    def test_fail():
>       assert 0
e       assert 0

test_report.py:35: assertionerror
=============================================== short test summary info ===============================================
skipped [1] d:\personal files\python\pytest-chinese-doc\src\test_report.py:44: skipping this test
xfail test_report.py::test_xfail
  reason: xfailing this test
xpass test_report.py::test_xpass always xfail
error test_report.py::test_error - assert 0
failed test_report.py::test_fail - assert 0
======================== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.10s ========================

-r选项后面要紧接这一个参数,用于过滤显示测试用例的结果。a表示除了成功的其它所有的测试用例。

以下是所有有效的字符参数:

  • f:失败的
  • e:出错的
  • s:跳过执行的
  • x:跳过执行,并标记为xfailed的
  • x:跳过执行,并标记为xpassed的
  • p:测试通过的
  • p:测试通过,并且有输出信息的;即用例中有print
  • a:除了测试通过的,其他所有的;即除了pp
  • a:所有的

上述字符参数可以叠加使用,例如:我们期望过滤出失败的和未执行的:

pytest -rfs

失败时加载pdb(python debugger)环境

pdb是python内建的诊断器,pytest允许通过以下命令在执行失败时进入这个诊断器模式:

pytest --pdb

pytest会在测试用例失败(或者ctrl+c)时,调用这个诊断器:

luyao@nj-luyao-t460 /d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest --pdb test_class.py
================================================= test session starts ================================================= 
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: d:\personal files\python\pytest-chinese-doc\src
collected 2 items

test_class.py .f
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

self = <src.test_class.testclass object at 0x00000269030ecb70>

    def test_two(self):
        x = 'hello'
>       assert hasattr(x, 'check')
e       assertionerror: assert false
e        +  where false = hasattr('hello', 'check')

test_class.py:30: assertionerror
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering pdb >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> pdb post_mortem (io-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 
> d:\personal files\python\pytest-chinese-doc\src\test_class.py(30)test_two()
-> assert hasattr(x, 'check')
(pdb) x
'hello'
(pdb)

此时,你可以访问测试用例的本地变量x;

并且,失败的信息存储在sys.last_value, sys.last_type, sys.last_traceback变量中,你可以在交互环境中访问它们:

(pdb) import sys
(pdb) sys.last_value
assertionerror("assert false\n +  where false = hasattr('hello', 'check')")
(pdb) sys.last_type
<class 'assertionerror'>
(pdb) sys.last_traceback
<traceback object at 0x00000269030f0908>
(pdb)

开始执行时就加载pdb环境

通过以下命令,pytest允许你在每个测试用例执行时,就加载pdb环境:

pytest --trace

设置断点

在测试用例代码中添加import pdb;pdb.set_trace(),当其被调用时,pytest会停止这条用例的输出:

  • 其他用例不受影响;
  • 通过continue命令,退出pdb环境,并继续执行用例;

使用内置的中断函数

python 3.7介绍了一个内置breakpoint()函数。pytest可以在以下场景中支持使用:

  • breakpoint()被调用,并且pythonbreakpoint未设置时,pytest会使用内部自定义的pdb代替系统的pdb;
  • 测试执行结束时,自动切回系统自带的pdb;
  • 当加上--pdb选项时,breakpoint()和测试发生错误时,都会调用内部自定义的pdb;
  • --pdbcls选项允许指定一个用户自定义的pdb类;

分析测试执行时间

获取执行最慢的10个测试用例:

pytest --durations=10

默认情况下,pytest不会显示执行时间<0.01s的测试用例,可以使用-vv选项查看它们;

错误句柄

5.0版本新增特性

在测试执行中发生段错误或者超时的情况下,faulthandler标准模块可以转储python的回溯信息;

它在pytest的执行中默认使能,使用-p no:faulthandler选项可以关闭它;

同样,faulthandler_timeout=x配置项,可用于当测试用例的完成时间超过x秒时,转储所有线程的python回溯信息;

# pytest.ini

[pytest]
faulthandler_timeout=5

配置测试执行的超时时间是5秒;

# test_fault_handler.py 

import time


def test_fault_handler():
    time.sleep(10)
    assert 1

测试用例中添加等待10秒的操作;

yaomengdemacbook-air:src yaomeng$ pytest test_fault_handler.py 
============================================================ test session starts =============================================================
platform darwin -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: /users/yaomeng/private/projects/pytest-chinese-doc/src, inifile: pytest.ini
plugins: repeat-0.8.0
collected 1 item                                                                                                                             

test_fault_handler.py timeout (0:00:05)!
thread 0x000000010d5895c0 (most recent call first):
  file "/users/yaomeng/private/projects/pytest-chinese-doc/src/test_fault_handler.py", line 26 in test_fault_handler
  file "/usr/local/lib/python3.7/site-packages/_pytest/python.py", line 170 in pytest_pyfunc_call
  file "/usr/local/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall
  file "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 81 in <lambda>
  file "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 87 in _hookexec
  file "/usr/local/lib/python3.7/site-packages/pluggy/hooks.py", line 289 in __call__
  file "/usr/local/lib/python3.7/site-packages/_pytest/python.py", line 1423 in runtest
  file "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 117 in pytest_runtest_call
  file "/usr/local/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall
  file "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 81 in <lambda>
  file "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 87 in _hookexec
  file "/usr/local/lib/python3.7/site-packages/pluggy/hooks.py", line 289 in __call__
  file "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 192 in <lambda>
  file "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 220 in from_call
  file "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 192 in call_runtest_hook
  file "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 167 in call_and_report
  file "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 87 in runtestprotocol
  file "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 72 in pytest_runtest_protocol
  file "/usr/local/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall
  file "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 81 in <lambda>
  file "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 87 in _hookexec
  file "/usr/local/lib/python3.7/site-packages/pluggy/hooks.py", line 289 in __call__
  file "/usr/local/lib/python3.7/site-packages/_pytest/main.py", line 256 in pytest_runtestloop
  file "/usr/local/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall
  file "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 81 in <lambda>
  file "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 87 in _hookexec
  file "/usr/local/lib/python3.7/site-packages/pluggy/hooks.py", line 289 in __call__
  file "/usr/local/lib/python3.7/site-packages/_pytest/main.py", line 235 in _main
  file "/usr/local/lib/python3.7/site-packages/_pytest/main.py", line 191 in wrap_session
  file "/usr/local/lib/python3.7/site-packages/_pytest/main.py", line 228 in pytest_cmdline_main
  file "/usr/local/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall
  file "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 81 in <lambda>
  file "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 87 in _hookexec
  file "/usr/local/lib/python3.7/site-packages/pluggy/hooks.py", line 289 in __call__
  file "/usr/local/lib/python3.7/site-packages/_pytest/config/__init__.py", line 78 in main
  file "/usr/local/bin/pytest", line 10 in <module>
.                                                                                                                [100%]

============================================================= 1 passed in 10.02s =============================================================

在执行刚超过5秒的时候会打印出回溯信息。但不会中断测试的执行;

yaomengdemacbook-air:src yaomeng$ pytest -p no:faulthandler test_fault_handler.py 
============================================================ test session starts =============================================================
platform darwin -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: /users/yaomeng/private/projects/pytest-chinese-doc/src, inifile: pytest.ini
plugins: repeat-0.8.0
collected 1 item 

test_fault_handler.py .                                                                                            [100%]

============================================================= 1 passed in 10.02s =============================================================

当去使能faulthandler的时,超时并不会触发回溯信息的打印;

注意:这个功能是从插件合并而来的,但是有两点不同:

  • 去使能时,使用-p no:faulthandler代替原来的--no-faulthandler;
  • 使用faulthandler_timeout配置项代替--faulthandler-timeout命令行选项来配置超时时间。当然,你也可以使用-o faulthandler_timeout=x在命令行配置;

创建junitxml格式的结果报告

使用如下命令,可以创建一个能被jenkins或者其他ci工具读取的测试结果文件:

/d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest test_class.py --junitxml=../report/test_class.xml

它会在指定目录下创建一个xml文件:

<!--test_class.xml-->
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
    <testsuite errors="0" failures="1" hostname="nj-luyao-t460" name="pytest" skipped="0" tests="2" time="0.058"
        timestamp="2019-09-10t09:53:07.554001">
        <testcase classname="test_class.testclass" file="test_class.py" line="23" name="test_one" time="0.001">
        </testcase>
        <testcase classname="test_class.testclass" file="test_class.py" line="27" name="test_two" time="0.002">
            <failure message="assertionerror: assert false
 +  where false = hasattr(&apos;hello&apos;, &apos;check&apos;)">self = &lt;src.test_class.testclass object at
                0x000001773e688c50&gt;

                def test_two(self):
                x = &apos;hello&apos;
                &gt; assert hasattr(x, &apos;check&apos;)
                e assertionerror: assert false
                e + where false = hasattr(&apos;hello&apos;, &apos;check&apos;)

                test_class.py:30: assertionerror</failure>
        </testcase>
    </testsuite>
</testsuites>

你可以在pytest.ini文件中设置junit_suite_name,来修改xml文件中testsuite根节点的name信息:

# pytest.ini

[pytest]
junit_suite_name = pytest_chinese_doc
/d/personal files/python/pytest-chinese-doc/src (5.1.2)
pytest test_class.py test_sample.py --junitxml=../report/test_mysuitename.xml
<!--test_mysuitename.xml-->
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
    <testsuite errors="0" failures="2" hostname="nj-luyao-t460" name="pytest_chinese_doc" skipped="0" tests="3"
        time="0.070" timestamp="2019-09-10t10:07:29.078541">
        <testcase classname="test_class.testclass" file="test_class.py" line="23" name="test_one" time="0.000">
        </testcase>
        <testcase classname="test_class.testclass" file="test_class.py" line="27" name="test_two" time="0.001">
            <failure message="assertionerror: assert false
 +  where false = hasattr(&apos;hello&apos;, &apos;check&apos;)">self = &lt;src.test_class.testclass object at
                0x000001c7f1155550&gt;

                def test_two(self):
                x = &apos;hello&apos;
                &gt; assert hasattr(x, &apos;check&apos;)
                e assertionerror: assert false
                e + where false = hasattr(&apos;hello&apos;, &apos;check&apos;)

                test_class.py:30: assertionerror</failure>
        </testcase>
        <testcase classname="test_sample" file="test_sample.py" line="26" name="test_sample" time="0.001">
            <failure message="assert 4 == 5
 +  where 4 = func(3)">def test_sample():
                &gt; assert func(3) == 5
                e assert 4 == 5
                e + where 4 = func(3)

                test_sample.py:28: assertionerror</failure>
        </testcase>
    </testsuite>
</testsuites>

可以看到,<testsuite errors="0" failures="2" hostname="nj-luyao-t460" name="pytest_chinese_doc"的name信息已经从默认的pytest变成我们期望的pytest_chinese_doc;

注意:这是4.0版本的新增配置项;

junit xml规定time属性应该表明测试用例执行的全部耗时,包含setupteardown中的操作,这也是pytest的默认行为;如果你只想记录测试用例执行的时间,只需要做如下配置:

# pytest.ini

junit_duration_report = call

record_property

如果你想在一个测试用例的报告中添加一些额外的属性节点,可以使用record_property fixture:

# test_record_property.py

def test_record_property(record_property):
    record_property('example_key', 1)
    assert 1

我们期望添加一个example_key属性节点;

/d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest test_record_property.py --junitxml=../report/test_record_property.xml
<!--test_record_property.xml-->
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
    <testsuite errors="0" failures="0" hostname="nj-luyao-t460" name="pytest_chinese_doc" skipped="0" tests="1"
        time="0.025" timestamp="2019-09-10t10:24:20.796619">
        <testcase classname="test_record_property" file="test_record_property.py" line="22" name="test_record_property"
            time="0.001">
            <properties>
                <property name="example_key" value="1" />
            </properties>
        </testcase>
    </testsuite>
</testsuites>

这样,我们就为test_record_property用例添加了一个example_key="1"的新属性;

或者,你可以将这种功能集成到一个自定义的标记中,例如:允许为测试用例添加一个测试id:

# conftest.py

def pytest_collection_modifyitems(session, config, items):
    for item in items:
        for marker in item.iter_markers(name='test_id'):
            test_id = marker.args[0]
            item.user_properties.append(('test_id', test_id))

为以下用例标记一个测试id:

# test_testid.py
import pytest


@pytest.mark.test_id(10010)
def test_record_property(record_property):
    record_property('example_key', 1)
    assert 1
/d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest test_testid.py --junitxml=../report/test_testid.xml
================================================= test session starts ================================================= 
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: d:\personal files\python\pytest-chinese-doc\src, inifile: pytest.ini
collected 1 item

test_testid.py .                                                                                                 [100%]

================================================== warnings summary =================================================== 
d:\program files\python\python37\lib\site-packages\_pytest\mark\structures.py:324
  d:\program files\python\python37\lib\site-packages\_pytest\mark\structures.py:324: pytestunknownmarkwarning: unknown pytest.mark.test_id - is this a typo?  you can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    pytestunknownmarkwarning,

-- docs: https://docs.pytest.org/en/latest/warnings.html
----------- generated xml file: d:\personal files\python\pytest-chinese-doc\report\test_record_property.xml ----------- ============================================ 1 passed, 1 warnings in 0.03s ============================================

这里有个告警,提示我们没有注册自定义的标记,但并不影响执行。我们只需要在pytest.ini中注册一下就可以了:

# pytest.ini

[pytest]
markers =
    test_id: 为测试用例添加id
<!--test_testid.xml-->
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
    <testsuite errors="0" failures="0" hostname="nj-luyao-t460" name="pytest_chinese_doc" skipped="0" tests="1"
        time="0.025" timestamp="2019-09-10t10:46:50.694771">
        <testcase classname="test_testid" file="test_testid.py" line="24" name="test_record_property" time="0.002">
            <properties>
                <property name="test_id" value="10010" />
                <property name="example_key" value="1" />
            </properties>
        </testcase>
    </testsuite>
</testsuites>

可以看到,我们又为test_record_property用例添加了一个test_id="10010"的新属性;

注意:这样使用可能会使xml文件不符合最新的junitxml的模式检查规则,导致在某些ci工具可能会发生未知的错误;

record_xml_attribute

如果你想在testcase节点中添加一个新的属性,可以使用record_xml_attribute fixture。如果这个属性之前已经存在了,那么就会用新值覆盖掉原来的值:

# test_record_attribute.py

def test_record_property(record_xml_attribute):
    record_xml_attribute('example_key', 1)
    record_xml_attribute('classname', 'overwrite_classname')
    assert 1

添加一个新的example_key属性,并修改classname属性;

/d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest test_record_attribute.py --junitxml=../report/test_record_attribute.xml
================================================= test session starts =================================================
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: d:\personal files\python\pytest-chinese-doc\src, inifile: pytest.ini
collected 1 item

test_record_attribute.py .                                                                                       [100%]

================================================== warnings summary =================================================== test_record_attribute.py::test_record_attribute
  test_record_attribute.py:23: pytestexperimentalapiwarning: record_xml_attribute is an experimental feature
    def test_record_attribute(record_xml_attribute):

-- docs: https://docs.pytest.org/en/latest/warnings.html
---------- generated xml file: d:\personal files\python\pytest-chinese-doc\report\test_record_attribute.xml ----------- ============================================ 1 passed, 1 warnings in 0.03s ============================================
<!--test_record_attribute.xml-->
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
    <testsuite errors="0" failures="0" hostname="nj-luyao-t460" name="pytest_chinese_doc" skipped="0" tests="1"
        time="0.026" timestamp="2019-09-10t11:00:57.818851">
        <testcase classname="overwrite_classname" example_key="1" file="test_record_attribute.py" line="22"
            name="test_record_attribute" time="0.002"></testcase>
    </testsuite>
</testsuites>

可以看到,<testcase classname="overwrite_classname" example_key="1"节点的classname被替换,并且新增了example_key属性;

注意:record_xml_attribute目前是一个实验性的功能,未来可能被更强大的api所替代,但功能本身会被保留。

注意:这样使用可能会使xml文件不符合最新的junitxml的模式检查规则,导致在某些ci工具可能会发生未知的错误;

record_testsuite_property

4.5版本新增功能

如果你想在testsuite级别上添加属性节点,并且希望其作用于所有的测试用例,那么你可以在session作用域中使用record_testsuite_property fixture:

# conftest.py

@pytest.fixture(scope="session", autouse=true)
def log_global_env_facts(record_testsuite_property):
    record_testsuite_property("arch", "ppc")
    record_testsuite_property("storage_type", "ceph")

record_testsuite_property接收两个参数namevalue以构成<property>标签,其中,name必须为字符串,value会转换为字符串并进行xml转义;

# test_record_testsuite_property.py

class testme:
    def test_foo(self):
        assert true

log_global_env_facts为session级别的fixture,并且,autouse为true,所以并不需要事先引入;

/d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest test_record_testsuite_property.py --junitxml=../report/test_record_testsuite_property.xml
<!--test_record_testsuite_property.xml-->
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
    <testsuite errors="0" failures="0" hostname="nj-luyao-t460" name="pytest_chinese_doc" skipped="0" tests="1"
        time="0.029" timestamp="2019-09-10t11:20:46.123978">
        <properties>
            <property name="arch" value="ppc" />
            <property name="storage_type" value="ceph" />
        </properties>
        <testcase classname="test_record_testsuite_property.testme" file="test_record_testsuite_property.py" line="23"
            name="test_foo" time="0.001"></testcase>
    </testsuite>
</testsuites>

可以看到,在testsuite节点中,新增了一个properties属性节点,包含所有新增的属性,而且,它和testcase节点是兄弟节点;

注意:这样生成的xml文件是符合最新的xunit标准的,这点和record_propertyrecord_xml_attribute正好相反;

为测试报告提供在线的pastebin展示

目前,只实现了在http://bpaste.net上的展示功能;

为每一个失败用例创建url

luyao@nj-luyao-t460 /d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest test_class.py --pastebin=failed
================================================= test session starts =================================================
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0 -- d:\program files\python\python37\python.exe
rootdir: d:\personal files\python\pytest-chinese-doc\src, inifile: pytest.ini
collected 2 items

test_class.py .f                                                                                                 [100%]

====================================================== failures ======================================================= 
_________________________________________________ testclass.test_two __________________________________________________

self = <src.test_class.testclass object at 0x000001ab97f29208>

    def test_two(self):
        x = 'hello'
>       assert hasattr(x, 'check')
e       assertionerror: assert false
e        +  where false = hasattr('hello', 'check')

test_class.py:30: assertionerror
======================================== sending information to paste service ========================================= 
test_class.py:30: assertionerror --> https://bpaste.net/show/hgeb
============================================= 1 failed, 1 passed in 5.64s =============================================

在浏览器中访问https://bpaste.net/show/hgeb,就能查看到失败的信息了。

为所有的测试用例创建url

luyao@nj-luyao-t460 /d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ pytest test_class.py --pastebin=all
================================================= test session starts =================================================
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0 -- d:\program files\python\python37\python.exe
rootdir: d:\personal files\python\pytest-chinese-doc\src, inifile: pytest.ini
collected 2 items

test_class.py .f                                                                                                 [100%]

====================================================== failures ======================================================= 
_________________________________________________ testclass.test_two __________________________________________________

self = <src.test_class.testclass object at 0x00000218030f91d0>

    def test_two(self):
        x = 'hello'
>       assert hasattr(x, 'check')
e       assertionerror: assert false
e        +  where false = hasattr('hello', 'check')

test_class.py:30: assertionerror
============================================= 1 failed, 1 passed in 0.06s ============================================= 
======================================== sending information to paste service =========================================
pastebin session-log: https://bpaste.net/show/xdvt

在浏览器中访问https://bpaste.net/show/xdvt,就能查看到所有用例的执行信息了。

尽量早的加载插件

你可以在命令行中使用-p选项,来尽量早的加载某一个插件:

pytest -p mypluginmodule

-p选项接收一个name参数,这个参数可以为:

  • 一个完整的本地插件引入,例如:myproject.plugins,其必须是可以import的。
  • 一个公共插件的名称,这是其注册时在setuptools中赋予的名字,例如:

去使能插件

你可以在命令行中使用-p结合no:,来去使能一个插件的加载,例如:

pytest -p no:doctest

在python代码中调用pytest

你可以在代码中直接调用pytest:

pytest.main()

这和你在命令行中执行pytest是一样的,不过它不会触发systemexit,而是返回:

# invoke_with_main.py

import time


def test_one():
    time.sleep(10)
    assert true


if __name__ == '__main__':
    import pytest
    ret = pytest.main([__file__])
    print(ret == pytest.exitcode.interrupted)

用例中有等待10秒的操作,当我们在这期间,打断执行(ctr+c),我们期望pytest.main()返回的状态码是interrupted,也就是2

/d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ python invoke_with_main.py
================================================= test session starts =================================================
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: d:\personal files\python\pytest-chinese-doc\src, inifile: pytest.ini
collected 1 item

invoke_with_main.py

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! keyboardinterrupt !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
d:\personal files\python\pytest-chinese-doc\src\invoke_with_main.py:26: keyboardinterrupt
(to show a full traceback on keyboardinterrupt use --full-trace)
================================================ no tests ran in 2.11s ================================================
true

可以看到,pytest.main()确实返回了interrupted的状态码;

你也可以传递选项和参数:

pytest.main(["-x", "mytestdir"])

同样,也可以为pytest.main()指定一个自定义的插件:

# invoke_with_plugins.py

class myplugin:
    def pytest_sessionfinish(self):
        print('*** test run reporting finishing')


if __name__ == '__main__':
    import pytest
    pytest.main(['test_class.py'], plugins=[myplugin()])

执行这个文件,会发现插件正常使能,并且它的钩子方法也被调用:

/d/personal files/python/pytest-chinese-doc/src (5.1.2)
λ python invoke_with_plugins.py
================================================= test session starts =================================================
platform win32 -- python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.12.0
rootdir: d:\personal files\python\pytest-chinese-doc\src, inifile: pytest.ini
collected 2 items

test_class.py .f                                                                                                 [100%]
*** test run reporting finishing


====================================================== failures ======================================================= 
_________________________________________________ testclass.test_two __________________________________________________

self = <src.test_class.testclass object at 0x00000236949a20b8>

    def test_two(self):
        x = 'hello'
>       assert hasattr(x, 'check')
e       assertionerror: assert false
e        +  where false = hasattr('hello', 'check')

test_class.py:30: assertionerror
============================================= 1 failed, 1 passed in 0.06s =============================================

注意:调用pytest.main()会引入你的测试文件以及其引用的所有模块。由于python引入机制的缓存特性,当这些文件发生变化时,后续再调用pytest.main()(在同一个程序执行过程中)时,并不会响应这些文件的变化。基于这个原因,我们不推荐在同一个程序中多次调用pytest.main()(例如:为了重新执行测试;如果你确实有这个需求,或许可以考虑插件);