在unittest TestCases中参数化测试。
项目描述
在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”。别被这吓到了…)
参数化测试用例有两个步骤
在测试用例的基类中使用 ParametrizedTestCase。
将 @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 的较小替代品,目标如下
仅支持 unittest 测试用例。对于其他类型的测试,您可以使用 pytest 的参数化。
避免任何自定义测试运行器支持。在定义时修改类意味着所有测试运行器将以相同的方式看到测试。
使用现代Python特性,如 __init_subclass__。
实现完整的类型提示覆盖。当在严格模式下使用Mypy时,你不应该发现unittest-parametrize是一个障碍。
使用“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)重新排序。
由于测试方法运行多个测试,子测试会扭曲测试时间。
为了循环和上下文管理器,所有内容都额外缩进两级。
参数化通过创建单独的测试方法避免了所有这些问题。
项目详情
下载文件
下载适合您平台的文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。