跳转到主要内容

在unittest TestCases中参数化测试。

项目描述

https://img.shields.io/github/actions/workflow/status/adamchainz/unittest-parametrize/main.yml?branch=main&style=for-the-badge https://img.shields.io/pypi/v/unittest-parametrize.svg?style=for-the-badge https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge pre-commit

在unittest TestCases中参数化测试。

安装

安装方式

python -m pip install unittest-parametrize

支持Python 3.8至3.12。


测试Django项目? 请查看我的书籍 Speed Up Your Django Tests,其中涵盖了大量编写更快、更准确测试的建议。


用法

API尽可能镜像 @pytest.mark.parametrize。 (甚至比稍常见的 parameterize 多一个“e”。别被这吓到了…)

参数化测试用例有两个步骤

  1. 在测试用例的基类中使用 ParametrizedTestCase

  2. @parametrize 应用到任何需要参数化的测试。此装饰器接受(至少)

    • 作为逗号分隔的字符串的参数化参数名称

    • 参数元组列表,为每个参数创建单独的测试

以下是一个基本示例

from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class SquareTests(ParametrizedTestCase):
    @parametrize(
        "x,expected",
        [
            (1, 1),
            (2, 4),
        ],
    )
    def test_square(self, x: int, expected: int) -> None:
        self.assertEqual(x**2, expected)

@parametrize 使用Python的 __init_subclass__ 钩子 在定义时修改类。它删除原始测试方法并创建具有单独名称的包装副本。因此,无论您使用哪种测试运行器(无论是unittest、Django的测试运行器、pytest等),参数化都应该正常工作。

提供参数名称作为单独的字符串

您也可以提供参数名称作为字符串序列

from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class SquareTests(ParametrizedTestCase):
    @parametrize(
        ("x", "expected"),
        [
            (1, 1),
            (2, 4),
        ],
    )
    def test_square(self, x: int, expected: int) -> None:
        self.assertEqual(x**2, expected)

在您的基测试用例类中使用 ParametrizedTestCase

ParametrizedTestCase 如果一个类中没有用 @parametrize 装饰的测试,则不会做任何事情。因此,您可以将它包含在项目的基测试用例类中,以便在所有测试用例中立即使用 @parametrize

例如,在 Django 项目中,您可以在 example.test 这样的模块中创建一组项目特定的基测试用例类,继承自 Django 提供的类。您可以在整个测试套件中使用这些基类。要将 ParametrizedTestCase 添加到所有副本,请在自定义 SimpleTestCase 中使用它,然后使用多继承将其混入其他类,如下所示

from django import test
from unittest_parametrize import ParametrizedTestCase


class SimpleTestCase(ParametrizedTestCase, test.SimpleTestCase):
    pass


class TestCase(SimpleTestCase, test.TestCase):
    pass


class TransactionTestCase(SimpleTestCase, test.TransactionTestCase):
    pass


class LiveServerTestCase(SimpleTestCase, test.LiveServerTestCase):
    pass

自定义测试名称后缀

默认情况下,测试名称将添加一个索引,从零开始。当运行测试时,您可以查看这些名称

$ python -m unittest t.py -v
test_square_0 (t.SquareTests.test_square_0) ... ok
test_square_1 (t.SquareTests.test_square_1) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

您可以通过传递包含参数和可选后缀 ID 的 param 对象来自定义这些名称

from unittest_parametrize import param
from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class SquareTests(ParametrizedTestCase):
    @parametrize(
        "x,expected",
        [
            param(1, 1, id="one"),
            param(2, 4, id="two"),
        ],
    )
    def test_square(self, x: int, expected: int) -> None:
        self.assertEqual(x**2, expected)

生成可能更自然的名称

$ python -m unittest t.py -v
test_square_one (t.SquareTests.test_square_one) ... ok
test_square_two (t.SquareTests.test_square_two) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

参数 ID 应该是有效的 Python 标识符后缀。

由于参数 ID 是可选的,因此您只需要为一些测试提供它们

from unittest_parametrize import param
from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class SquareTests(ParametrizedTestCase):
    @parametrize(
        "x,expected",
        [
            param(1, 1),
            param(20, 400, id="large"),
        ],
    )
    def test_square(self, x: int, expected: int) -> None:
        self.assertEqual(x**2, expected)

无 ID 的 param 将回退到默认索引后缀

$ python -m unittest t.py -v
test_square_0 (example.SquareTests.test_square_0) ... ok
test_square_large (example.SquareTests.test_square_large) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

或者,您可以使用 ids 参数分别提供 ID

from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class SquareTests(ParametrizedTestCase):
    @parametrize(
        "x,expected",
        [
            (1, 1),
            (2, 4),
        ],
        ids=["one", "two"],
    )
    def test_square(self, x: int, expected: int) -> None:
        self.assertEqual(x**2, expected)

与其他测试装饰器一起使用

@parametrize 尝试确保它是最高层(最外层)装饰器。存在这种限制是为了确保其他装饰器应用于每个参数化测试。因此,像 @mock.patch 这样的装饰器需要位于 @parametrize 之下

from unittest import mock
from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class CarpentryTests(ParametrizedTestCase):
    @parametrize(
        "nails",
        [(11,), (17,)],
    )
    @mock.patch("example.hammer", autospec=True)
    def test_nail_a_board(self, mock_hammer, nails):
        ...

另外,由于 mock.patch 总是在开始处添加位置参数,因此参数化参数必须放在最后。 @parametrize 总是添加参数作为关键字参数,因此您也可以为参数化参数使用 关键字参数语法

# ...
def test_nail_a_board(self, mock_hammer, *, nails):
    ...

多个 @parametrize 装饰器

@parametrize 不可堆叠。要创建测试的笛卡尔积,可以使用嵌套列表解析

from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class RocketTests(ParametrizedTestCase):
    @parametrize(
        "use_ions,hyperdrive_level",
        [
            (use_ions, hyperdrive_level)
            for use_ions in [True, False]
            for hyperdrive_level in [0, 1, 2]
        ],
    )
    def test_takeoff(self, use_ions, hyperdrive_level) -> None:
        ...

上述代码创建了 test_takeoff 的 2 * 3 = 6 个版本。

对于更大的组合, itertools.product() 可能更易于阅读

from itertools import product
from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class RocketTests(ParametrizedTestCase):
    @parametrize(
        "use_ions,hyperdrive_level,nose_colour",
        list(
            product(
                [True, False],
                [0, 1, 2],
                ["red", "yellow"],
            )
        ),
    )
    def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None:
        ...

上述代码创建了 test_takeoff 的 2 * 3 * 2 = 12 个版本。

在测试用例中参数化多个测试

@parametrize 只能作为函数装饰器使用,不能作为类装饰器。要参数化测试用例中的所有测试,请创建一个单独的装饰器并将其应用于每个方法

from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


parametrize_race = parametrize(
    "race",
    [("Human",), ("Halfling",), ("Dwarf",), ("Elf",)],
)


class StatsTests(ParametrizedTestCase):
    @parametrize_race
    def test_strength(self, race: str) -> None:
        ...

    @parametrize_race
    def test_dexterity(self, race: str) -> None:
        ...

    ...

历史

当我开始编写单元测试时,我学会了使用 DDT (数据驱动测试) 来参数化测试。它有效,但文档有点少,API 有点难以理解(@ddt 再次代表什么?)。

后来当我学习 pytest 时,我学会了使用它的 参数化 API。它易于阅读且灵活,但它不适用于 unittest 测试用例,这是 Django 的测试工具提供的。

因此,直到创建此包之前,我在我的 (Django) 测试用例中使用了 parameterized。此包支持跨多个测试运行器进行参数化,尽管其中大部分现在都是“旧版”的。

我创建 unittest-parametrize 作为 parameterized 的较小替代品,目标如下

  1. 仅支持 unittest 测试用例。对于其他类型的测试,您可以使用 pytest 的参数化。

  2. 避免任何自定义测试运行器支持。在定义时修改类意味着所有测试运行器将以相同的方式看到测试。

  3. 使用现代Python特性,如 __init_subclass__

  4. 实现完整的类型提示覆盖。当在严格模式下使用Mypy时,你不应该发现unittest-parametrize是一个障碍。

  5. 使用“parametrize”这个名字而不是“parameterize”。这种与pytest的拼写统一有助于减少对额外“e”的混淆。

感谢ddt、parameterized和pytest的创建者和维护者的辛勤工作。

为什么不使用子测试呢?

TestCase.subTest() 是unittest内置的“参数化”解决方案。你可以在单个测试方法内的循环中使用它

from unittest import TestCase


class SquareTests(TestCase):
    def test_square(self):
        tests = [
            (1, 1),
            (2, 4),
        ]
        for x, expected in tests:
            with self.subTest(x=x):
                self.assertEqual(x**2, expected)

这种方法将多个实际测试压缩到一个测试方法中,有几个后果

  • 如果子测试失败,它将阻止后续的子测试运行。因此,失败更难以调试,因为每次测试运行只能提供部分信息。

  • 子测试可能会泄露状态。如果没有正确的隔离,它们可能不会测试它们看起来要测试的内容。

  • 子测试不能被检测到状态泄露的工具(如 pytest-randomly)重新排序。

  • 由于测试方法运行多个测试,子测试会扭曲测试时间。

  • 为了循环和上下文管理器,所有内容都额外缩进两级。

参数化通过创建单独的测试方法避免了所有这些问题。

项目详情


下载文件

下载适合您平台的文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。

源分布

unittest_parametrize-1.4.0.tar.gz (12.5 kB 查看哈希值)

上传时间

构建分布

unittest_parametrize-1.4.0-py3-none-any.whl (8.1 kB 查看哈希值)

上传时间 Python 3

由以下支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面