安装和入门#

pytest 是一个能够简化测试系统构建、方便测试规模扩展的框架,它让测试变得更具表现力和可读性。只需要几分钟的时间,你就可以开始一个简单的单元测试或者复杂的功能测试。

安装 pytest#

  1. 在命令行执行以下命令:

    $ pip install pytest==6.1.1
  2. 检查版本:

    $ pytest --version
    pytest 6.1.1

创建你的第一个测试用例#

它只有四行代码:

# src/chapter-1/test_sample.py

def func(x):
    return x + 1


def test_answer():
    assert func(3) == 5

现在开始执行这个测试用例:

$ pipenv run pytest src/chapter-1/test_sample.py
================================ test session starts =================================
platform darwin -- Python 3.8.4, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: /Users/yaomeng/Private/projects/pytest-chinese-doc
collected 1 item

src/chapter-1/test_sample.py F                                                 [100%]

====================================== FAILURES ======================================
____________________________________ test_answer _____________________________________

    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

src/chapter-1/test_sample.py:6: AssertionError
============================== short test summary info ===============================
FAILED src/chapter-1/test_sample.py::test_answer - assert 4 == 5
================================= 1 failed in 0.05s ==================================

[100%]表示执行所有测试用例的总体进度。因为func(3)不等于5,所以最后 pytest 会显示一个表示测试失败的报告。

Note

在上面的例子中,我们直接使用assert来验证测试用例中的预期结果,这是因为 pytest 提供了高级的断言自省功能,可以智能的为我们展示失败处的中间信息(where 4 = func(3)),因此我们就无需使用assertEqualassertNotEqual之类的方法。

执行多个测试用例#

pytest 会执行当前及其子文件夹中,所有命名符合test_*.py或者*_test.py规则的文件中的所有的测试用例;

触发一个指定异常的断言#

使用raises可以验证某些代码是否抛出一个指定的异常:

# src/chapter-1/test_sysexit.py

import pytest


def f():
    # 请求退出解释器
    raise SystemExit(1)


def test_mytest():
    with pytest.raises(SystemExit):
        f()

执行这个测试用例时,加上-q选项可以精简测试结果的输出:

$ pipenv run pytest src/chapter-1/test_sysexit.py
================================ test session starts =================================
platform darwin -- Python 3.8.4, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: /Users/yaomeng/Private/projects/pytest-chinese-doc
collected 1 item

src/chapter-1/test_sysexit.py .                                                [100%]

================================= 1 passed in 0.01s ==================================

Note

使用-q/--quiet命令行标记可以简化测试结果输出。

在一个类中组织多个测试用例#

pytest 可以让你很容易的创建一个测试类来包含多个测试用例:

# src/chapter-1/test_class.py

class TestClass:
    def test_one(self):
        x = "this"
        assert "h" in x

    def test_two(self):
        x = "hello"
        assert hasattr(x, "check")

我们无需让测试类继承自任何基类,但是要确保类名的前缀为Test,否则将忽略其中的测试用例。通过传入文件路径就可以执行这个测试类:

$ pipenv run pytest -q src/chapter-1/test_class.py
.F                                                                             [100%]
====================================== FAILURES ======================================
_________________________________ TestClass.test_two _________________________________

self = <test_class.TestClass object at 0x1046f7df0>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

src/chapter-1/test_class.py:8: AssertionError
============================== short test summary info ===============================
FAILED src/chapter-1/test_class.py::TestClass::test_two - AssertionError: assert False
1 failed, 1 passed in 0.04s

从输出的结果中我们可以看到:

  • test_one测试成功,用.表示;test_two测试失败,用F表示;
  • 并且我们清楚的看到test_two失败处的中间值的信息:where False = hasattr('hello', 'check')

Warning

测试类要符合特定的规则,pytest 才能发现它:

  • 测试类的命名要符合Test*规则;
  • 测试类中不能有__init__()方法,否则 pytest 无法采集到其中的测试用例;
    • PytestCollectionWarning: cannot collect test class 'TestClass' because it has a __init__ constructor.

在类中组织多个测试用例的好处体现以下几个方面:

  • 结构化测试的组织;
  • 仅在指定的类中共享fixture
  • 应用在类上的marker将隐式的应用于其中所有的测试用例上;

需要注意的一点是,测试类中的每个用例都拥有该类的唯一实例。这是因为如果让所有测试用例共享同一个类实例,将不利于测试的隔离。

# src/chapter-1/test_class_demo.py

class TestClassDemoInstance:
    def test_one(self):
        assert 0

    def test_two(self):
        assert 0
$ pipenv run pytest -q src/chapter-1/test_class_demo.py
FF                                                                             [100%]
====================================== FAILURES ======================================
___________________________ TestClassDemoInstance.test_one ___________________________

self = <test_class_demo.TestClassDemoInstance object at 0x10ea69ee0>

    def test_one(self):
>       assert 0
E       assert 0

src/chapter-1/test_class_demo.py:3: AssertionError
___________________________ TestClassDemoInstance.test_two ___________________________

self = <test_class_demo.TestClassDemoInstance object at 0x10ea5c7f0>

    def test_two(self):
>       assert 0
E       assert 0

src/chapter-1/test_class_demo.py:6: AssertionError
============================== short test summary info ===============================
FAILED src/chapter-1/test_class_demo.py::TestClassDemoInstance::test_one - assert 0
FAILED src/chapter-1/test_class_demo.py::TestClassDemoInstance::test_two - assert 0
2 failed in 0.05s

可以看到,这个例子中的两个测试用例拥有不同的类实例:0x10ea69ee00x10ea5c7f0

申请一个唯一的临时目录#

pytest 提供一些内置的fixtures,用于请求一些系统的资源。例如,一个唯一的临时目录:

# src/chapter-1/test_tmpdir.py

def test_tempdir(tmpdir):
    print(tmpdir)
    assert 0

在这个例子中,在形参的位子列出tempdir fixture,pytest 会在每个用例执行之前创建一个唯一的临时目录。

现在,我们来执行这个测试用例:

$ pipenv run pytest -q src/chapter-1/test_tempdir.py
F                                                                              [100%]
====================================== FAILURES ======================================
____________________________________ test_tempdir ____________________________________

tmpdir = local('/private/var/folders/7r/2gv2hwyx6bj_wz30cb9r9b5r0000gn/T/pytest-of-yaomeng/pytest-0/test_tempdir0')

    def test_tempdir(tmpdir):
        print(tmpdir)
>       assert 0
E       assert 0

src/chapter-1/test_tempdir.py:3: AssertionError
-------------------------------- Captured stdout call --------------------------------
/private/var/folders/7r/2gv2hwyx6bj_wz30cb9r9b5r0000gn/T/pytest-of-yaomeng/pytest-0/test_tempdir0
============================== short test summary info ===============================
FAILED src/chapter-1/test_tempdir.py::test_tempdir - assert 0
1 failed in 0.04s

可以使用如下命令查看所有可用的 fixtures,如果想同时查看以_开头的 fixtures,需要添加-v选项:

$ pipenv run pytest -v --fixtures
================================ test session starts =================================
platform darwin -- Python 3.8.4, pytest-6.1.1, py-1.9.0, pluggy-0.13.1 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/bin/python
cachedir: .pytest_cache
rootdir: /Users/yaomeng/Private/projects/pytest-chinese-doc
collected 7 items
cache -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/cacheprovider.py:473
    Return a cache object that can persist state between testing sessions.

    cache.get(key, default)
    cache.set(key, value)

    Keys must be a ``/`` separated value, where the first part is usually the
    name of your plugin or application to avoid clashes with other cache users.

    Values can be any object handled by the json stdlib module.

capsys -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/capture.py:843
    Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.

    The captured output is made available via ``capsys.readouterr()`` method
    calls, which return a ``(out, err)`` namedtuple.
    ``out`` and ``err`` will be ``text`` objects.

capsysbinary -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/capture.py:860
    Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.

    The captured output is made available via ``capsysbinary.readouterr()``
    method calls, which return a ``(out, err)`` namedtuple.
    ``out`` and ``err`` will be ``bytes`` objects.

capfd -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/capture.py:877
    Enable text capturing of writes to file descriptors ``1`` and ``2``.

    The captured output is made available via ``capfd.readouterr()`` method
    calls, which return a ``(out, err)`` namedtuple.
    ``out`` and ``err`` will be ``text`` objects.

capfdbinary -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/capture.py:894
    Enable bytes capturing of writes to file descriptors ``1`` and ``2``.

    The captured output is made available via ``capfd.readouterr()`` method
    calls, which return a ``(out, err)`` namedtuple.
    ``out`` and ``err`` will be ``byte`` objects.

doctest_namespace [session scope] -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/doctest.py:738
    Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests.

pytestconfig [session scope] -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/fixtures.py:1398
    Session-scoped fixture that returns the :class:`_pytest.config.Config` object.

    Example::

        def test_foo(pytestconfig):
            if pytestconfig.getoption("verbose") > 0:
                ...

record_property -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/junitxml.py:305
    Add extra properties to the calling test.

    User properties become part of the test report and are available to the
    configured reporters, like JUnit XML.

    The fixture is callable with ``name, value``. The value is automatically
    XML-encoded.

    Example::

        def test_function(record_property):
            record_property("example_key", 1)

record_xml_attribute -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/junitxml.py:328
    Add extra xml attributes to the tag for the calling test.

    The fixture is callable with ``name, value``. The value is
    automatically XML-encoded.

record_testsuite_property [session scope] -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/junitxml.py:366
    Records a new ``<property>`` tag as child of the root ``<testsuite>``. This is suitable to
    writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family.

    This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:

    .. code-block:: python

        def test_foo(record_testsuite_property):
            record_testsuite_property("ARCH", "PPC")
            record_testsuite_property("STORAGE_TYPE", "CEPH")

    ``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.

caplog -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/logging.py:467
    Access and control log capturing.

    Captured logs are available through the following properties/methods::

    * caplog.messages        -> list of format-interpolated log messages
    * caplog.text            -> string containing formatted log output
    * caplog.records         -> list of logging.LogRecord instances
    * caplog.record_tuples   -> list of (logger_name, level, message) tuples
    * caplog.clear()         -> clear captured records and formatted log output string

monkeypatch -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/monkeypatch.py:29
    The returned ``monkeypatch`` fixture provides these
    helper methods to modify objects, dictionaries or os.environ::

        monkeypatch.setattr(obj, name, value, raising=True)
        monkeypatch.delattr(obj, name, raising=True)
        monkeypatch.setitem(mapping, name, value)
        monkeypatch.delitem(obj, name, raising=True)
        monkeypatch.setenv(name, value, prepend=False)
        monkeypatch.delenv(name, raising=True)
        monkeypatch.syspath_prepend(path)
        monkeypatch.chdir(path)

    All modifications will be undone after the requesting
    test function or fixture has finished. The ``raising``
    parameter determines if a KeyError or AttributeError
    will be raised if the set/deletion operation has no target.

recwarn -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/recwarn.py:29
    Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.

    See http://docs.python.org/library/warnings.html for information
    on warning categories.

tmpdir_factory [session scope] -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/tmpdir.py:155
    Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.


tmp_path_factory [session scope] -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/tmpdir.py:163
    Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.


tmpdir -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/tmpdir.py:179
    Return a temporary directory path object
    which is unique to each test function invocation,
    created as a sub directory of the base temporary
    directory.  The returned object is a `py.path.local`_
    path object.

    .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html

tmp_path -- ../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/tmpdir.py:192
    Return a temporary directory path object
    which is unique to each test function invocation,
    created as a sub directory of the base temporary
    directory.  The returned object is a :class:`pathlib.Path`
    object.

    .. note::

        in python < 3.6 this is a pathlib2.Path


=============================== no tests ran in 0.05s ================================