Skip to content

测试函数

断言

在 pytest 中,assert 是编写测试的最基础工具。如:

python
assert a == b
assert a <= b

具体的 assert 语法参考 The assert statement

捕获异常

在测试过程中,经常需要测试是否如期抛出预期的异常,以确定异常处理模块生效。在 pytest 中使用 pytest.raises() 进行异常捕获:

python
# filename: test02.py
import pytest


def test_raises():
    with pytest.raises(ZeroDivisionError) as e:
        1 / 0
    exec_msg = e.value.args[0]
    assert exec_msg == 'division by zero'

运行结果如下:

shell
============================= test session starts =============================
collecting ... collected 1 item

test02.py::test_raises PASSED                                            [100%]

============================== 1 passed in 0.05s ==============================

标记函数

Pytest 查找测试策略

默认情况下,pytest 会递归查找当前目录下所有以 test 开始或结尾的 Python 脚本,并执行文件内的所有以 test 开始或结束的函数和方法。

对于下面脚本:

python
# filename: test_no_mark.py
def test_func1():
    assert 1 == 1


def test_func2():
    assert 1 != 1

直接执行测试脚本会同时执行所有测试函数:

shell
$ pytest test_no_mark.py
================================= FAILURES =================================
________________________________ test_func2 ________________________________

    def test_func2():
>       assert 1 != 1
E       assert 1 != 1

test_no_mark.py:8: AssertionError
========================= short test summary info ==========================
FAILED test_no_mark.py::test_func2 - assert 1 != 1
======================= 1 failed, 1 passed in 0.12s ========================

标记测试函数

由于某种原因(如 test_func2 的功能尚未开发完成),我们只想执行指定的测试函数。在 pytest 中有几种方式可以解决:

第一种,显式指定函数名,通过 :: 标记。

shell
$ pytest.exe test_no_mark.py::test_func1
=========================== test session starts ============================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test
plugins: anyio-4.9.0, langsmith-0.3.34
collected 1 item

test_no_mark.py .                                                     [100%]

============================ 1 passed in 0.03s =============================

第二种,使用模糊匹配,使用 -k 选项标识。

shell
$ pytest -k func1 test_no_mark.py
=========================== test session starts ============================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test
plugins: anyio-4.9.0, langsmith-0.3.34
collected 2 items / 1 deselected / 1 selected

test_no_mark.py .                                                     [100%]

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

以上两种方法,第一种一次只能指定一个测试函数,当要进行批量测试时无能为力;第二种方法可以批量操作,但需要所有测试的函数名包含相同的模式,也不方便。

第三种,使用 pytest.mark 在函数上进行标记。

带标记的测试函数如:

python
# test_with_mark.py
import pytest


@pytest.mark.finished
def test_func1():
    assert 1 == 1


@pytest.mark.unfinished
def test_func2():
    assert 1 != 1

测试时使用 -m 选择标记的测试函数:

shell
$ pytest -m finished test_with_mark.py
=========================== test session starts ============================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test
plugins: anyio-4.9.0, langsmith-0.3.34
collected 0 items

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

使用 mark,我们可以给每个函数打上不同的标记,测试时指定就可以允许所有被标记的函数。

一个函数可以打多个标记;多个函数也可以打相同的标记。

运行测试时使用 -m 选项可以加上逻辑,如:

$ pytest -m "finished and commit"

$ pytest -m "finished and not merged"

跳过测试

上一节提到 pytest 使用标记过滤测试函数,所以对于那些尚未开发完成的测试,最好的处理方式就是略过而不执行测试。

按正向的思路,我们只要通过标记指定要测试的就可以解决这个问题;但有时候的处境是我们能进行反向的操作才是最好的解决途径,即通过标记指定要跳过的测试。

Pytest 使用特定的标记 pytest.mark.skip 完美的解决了这个问题。

python
# test_skip.py

@pytest.mark.skip(reason='out-of-date api')
def test_connect():
    pass

执行结果可以看到该测试已被忽略:

shell
$ pytest test_skip.py
=========================== test session starts ============================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test
plugins: anyio-4.9.0, langsmith-0.3.34
collected 1 item

test_skip.py s                                                        [100%]

============================ 1 skipped in 0.05s ============================

pytest 使用 s 表示测试被跳过(SKIPPED)。

Pytest 还支持使用 pytest.mark.skipif 为测试函数指定被忽略的条件。

python
@pytest.mark.skipif(conn.__version__ < '0.2.0',
                    reason='not supported until v0.2.0')
def test_api():
    pass

参数化

当对一个测试函数进行测试时,通常会给函数传递多组参数。比如测试账号登陆,我们需要模拟各种千奇百怪的账号密码。

当然,我们可以把这些参数写在测试函数内部进行遍历。不过虽然参数众多,但仍然是一个测试,当某组参数导致断言失败,测试也就终止了。

通过异常捕获,我们可以保证程所有参数完整执行,但要分析测试结果就需要做不少额外的工作。

在 pytest 中,我们有更好的解决方法,就是参数化测试,即每组参数都独立执行一次测试。使用的工具就是 pytest.mark.parametrize(argnames, argvalues)

这里是一个密码长度的测试函数,其中参数名为 passwd,其可选列表包含三个值:

python
# test_parametrize.py
import pytest


@pytest.mark.parametrize(
    'passwd', ['123456', 'abcdefdfs', 'as52345fasdf4']
)
def test_passwd_length(passwd):
    assert len(passwd) >= 8

运行可知执行了三次测试:

shell
$ pytest test_parametrize.py
========================= test session starts ==========================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test
plugins: anyio-4.9.0, langsmith-0.3.34
collected 3 items

test_parametrize.py ...                                           [100%]

========================== 3 passed in 0.06s ===========================

再看一个多参数的例子,用于校验用户密码:

python
# test_parametrize.py

@pytest.mark.parametrize(
    'user, passwd',
    [('jack', 'abcdefgh'), ('tom', 'a123456a')]
)
def test_passwd_md5(user, passwd):
    db = {
        'jack': 'e8dc4081b13434b45189a720b77b6818',
        'tom': '1702a132e769a623c1adb78353fc9503'
    }

    import hashlib

    assert hashlib.md5(passwd.encode()).hexdigest() == db[user]

使用 -v 执行测试

shell
$ pytest -v test_parametrize.py::test_passwd_md5
========================= test session starts ==========================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0 -- D:\software\Python312\python.exe
cachedir: .pytest_cache
rootdir: Z:\zhengxin\py_test
plugins: anyio-4.9.0, langsmith-0.3.34
collected 2 items

test_parametrize.py::test_passwd_md5[jack-abcdefgh] PASSED        [ 50%]
test_parametrize.py::test_passwd_md5[tom-a123456a] PASSED         [100%]

========================== 2 passed in 0.07s ===========================

如果觉得每组测试的默认参数显示不清晰,我们可以使用 pytest.paramid 参数进行自定义。

python
@pytest.mark.parametrize(
    'user, passwd',
    [
        pytest.param('jack', 'abcdefgh', id='User<Jack>'),
        pytest.param('tom', 'a123456a', id='User<Tom>')
    ]
)
def test_passwd_md5_id(user, passwd):
    db = {
        'jack': 'e8dc4081b13434b45189a720b77b6818',
        'tom': '1702a132e769a623c1adb78353fc9503'
    }

    import hashlib

    assert hashlib.md5(passwd.encode()).hexdigest() == db[user]

现在的执行结果为:

shell
$ pytest -v test_parametrize.py::test_passwd_md5_id
========================= test session starts ==========================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0 -- D:\software\Python312\python.exe
cachedir: .pytest_cache
rootdir: Z:\zhengxin\py_test
plugins: anyio-4.9.0, langsmith-0.3.34
collected 2 items

test_parametrize.py::test_passwd_md5_id[User<Jack>] PASSED        [ 50%]
test_parametrize.py::test_passwd_md5_id[User<Tom>] PASSED         [100%]

========================== 2 passed in 0.07s ===========================