使用任何Python测试框架进行参数化测试
项目描述
Python中的参数化测试很糟糕。
parameterized修复了这一点。针对所有内容。适用于nose的参数化测试,适用于py.test的参数化测试,适用于unittest的参数化测试。
# test_math.py
from nose.tools import assert_equal
from parameterized import parameterized, parameterized_class
import unittest
import math
@parameterized([
(2, 2, 4),
(2, 3, 8),
(1, 9, 1),
(0, 9, 0),
])
def test_pow(base, exponent, expected):
assert_equal(math.pow(base, exponent), expected)
class TestMathUnitTest(unittest.TestCase):
@parameterized.expand([
("negative", -1.5, -2.0),
("integer", 1, 1.0),
("large fraction", 1.6, 1),
])
def test_floor(self, name, input, expected):
assert_equal(math.floor(input), expected)
@parameterized_class(('a', 'b', 'expected_sum', 'expected_product'), [
(1, 2, 3, 2),
(5, 5, 10, 25),
])
class TestMathClass(unittest.TestCase):
def test_add(self):
assert_equal(self.a + self.b, self.expected_sum)
def test_multiply(self):
assert_equal(self.a * self.b, self.expected_product)
@parameterized_class([
{ "a": 3, "expected": 2 },
{ "b": 5, "expected": -4 },
])
class TestMathClassDict(unittest.TestCase):
a = 1
b = 1
def test_subtract(self):
assert_equal(self.a - self.b, self.expected)
使用nose(和nose2)
$ nosetests -v test_math.py test_floor_0_negative (test_math.TestMathUnitTest) ... ok test_floor_1_integer (test_math.TestMathUnitTest) ... ok test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok test_math.test_pow(2, 2, 4, {}) ... ok test_math.test_pow(2, 3, 8, {}) ... ok test_math.test_pow(1, 9, 1, {}) ... ok test_math.test_pow(0, 9, 0, {}) ... ok test_add (test_math.TestMathClass_0) ... ok test_multiply (test_math.TestMathClass_0) ... ok test_add (test_math.TestMathClass_1) ... ok test_multiply (test_math.TestMathClass_1) ... ok test_subtract (test_math.TestMathClassDict_0) ... ok ---------------------------------------------------------------------- Ran 12 tests in 0.015s OK
正如包名所暗示的,nose支持最好,将用于所有后续示例。
使用py.test(版本2.0及以上)
$ py.test -v test_math.py ============================= test session starts ============================== platform darwin -- Python 3.6.1, pytest-3.1.3, py-1.4.34, pluggy-0.4.0 collecting ... collected 13 items test_math.py::test_pow::[0] PASSED test_math.py::test_pow::[1] PASSED test_math.py::test_pow::[2] PASSED test_math.py::test_pow::[3] PASSED test_math.py::TestMathUnitTest::test_floor_0_negative PASSED test_math.py::TestMathUnitTest::test_floor_1_integer PASSED test_math.py::TestMathUnitTest::test_floor_2_large_fraction PASSED test_math.py::TestMathClass_0::test_add PASSED test_math.py::TestMathClass_0::test_multiply PASSED test_math.py::TestMathClass_1::test_add PASSED test_math.py::TestMathClass_1::test_multiply PASSED test_math.py::TestMathClassDict_0::test_subtract PASSED ==================== 12 passed, 4 warnings in 0.16 seconds =====================
使用unittest(和unittest2)
$ python -m unittest -v test_math test_floor_0_negative (test_math.TestMathUnitTest) ... ok test_floor_1_integer (test_math.TestMathUnitTest) ... ok test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok test_add (test_math.TestMathClass_0) ... ok test_multiply (test_math.TestMathClass_0) ... ok test_add (test_math.TestMathClass_1) ... ok test_multiply (test_math.TestMathClass_1) ... ok test_subtract (test_math.TestMathClassDict_0) ... ok ---------------------------------------------------------------------- Ran 8 tests in 0.001s OK
(注意:由于unittest不支持测试装饰器,只有使用@parameterized.expand创建的测试将被执行)
使用green
$ green test_math.py -vvv test_math TestMathClass_1 . test_method_a . test_method_b TestMathClass_2 . test_method_a . test_method_b TestMathClass_3 . test_method_a . test_method_b TestMathUnitTest . test_floor_0_negative . test_floor_1_integer . test_floor_2_large_fraction TestMathClass_0 . test_add . test_multiply TestMathClass_1 . test_add . test_multiply TestMathClassDict_0 . test_subtract Ran 12 tests in 0.121s OK (passes=9)
安装
$ pip install parameterized
兼容性
是(大多数情况)。
Py3.7 |
Py3.8 |
Py3.9 |
Py3.10 |
Py3.11 |
PyPy3 |
@mock.patch |
|
---|---|---|---|---|---|---|---|
nose |
是 |
是 |
是 |
是 |
否§ |
否§ |
是 |
nose2 |
是 |
是 |
是 |
是 |
是 |
是 |
是 |
py.test 2 |
否* |
否* |
否* |
否* |
否* |
否* |
否* |
py.test 3 |
是 |
是 |
是 |
是 |
否* |
否* |
是 |
py.test 4 |
否** |
否** |
否** |
否** |
否** |
否** |
否** |
py.test fixture |
否† |
否† |
否† |
否† |
否† |
否† |
否† |
unittest
(@parameterized.expand)
|
是 |
是 |
是 |
是 |
是 |
是 |
是 |
unittest2
(@parameterized.expand)
|
是 |
是 |
是 |
是 |
否§ |
否§ |
是 |
§:nose和unittest2 - 这两个最后更新于2015年 - 很遗憾似乎不支持Python 3.10或3.11。
*:[py.test 2似乎在Python 3下不工作](https://github.com/wolever/parameterized/issues/71),并且[py.test 3似乎在Python 3.10或3.11下不工作](https://github.com/wolever/parameterized/issues/154) (#71)。
**:py.test 4在[问题 #34](https://github.com/wolever/parameterized/issues/34)中尚未支持(但即将推出!)
†:py.test fixture的支持在[问题 #81](https://github.com/wolever/parameterized/issues/81)中有记录
依赖关系
(本节有意留空)
详尽的用法示例
@parameterized 和 @parameterized.expand 装饰器接受一个列表或元组的可迭代对象,或者一个返回列表或可迭代对象的函数。
from parameterized import parameterized, param
# A list of tuples
@parameterized([
(2, 3, 5),
(3, 5, 8),
])
def test_add(a, b, expected):
assert_equal(a + b, expected)
# A list of params
@parameterized([
param("10", 10),
param("10", 16, base=16),
])
def test_int(str_val, expected, base=10):
assert_equal(int(str_val, base=base), expected)
# An iterable of params
@parameterized(
param.explicit(*json.loads(line))
for line in open("testcases.jsons")
)
def test_from_json_file(...):
...
# A callable which returns a list of tuples
def load_test_cases():
return [
("test1", ),
("test2", ),
]
@parameterized(load_test_cases)
def test_from_function(name):
...
注意,当使用迭代器或生成器时,在测试运行开始之前,所有项目都将被加载到内存中(我们这样做是为了确保在多进程或多线程测试环境中,生成器被完全耗尽一次)。
@parameterized 装饰器可以用于测试类方法和独立函数。
from parameterized import parameterized
class AddTest(object):
@parameterized([
(2, 3, 5),
])
def test_add(self, a, b, expected):
assert_equal(a + b, expected)
@parameterized([
(2, 3, 5),
])
def test_add(a, b, expected):
assert_equal(a + b, expected)
@parameterized.expand 可以在测试生成器无法使用的情况下生成测试方法(例如,当测试类是 unittest.TestCase 的子类时)。
import unittest
from parameterized import parameterized
class AddTestCase(unittest.TestCase):
@parameterized.expand([
("2 and 3", 2, 3, 5),
("3 and 5", 3, 5, 8),
])
def test_add(self, _, a, b, expected):
assert_equal(a + b, expected)
将创建测试用例。
$ nosetests example.py test_add_0_2_and_3 (example.AddTestCase) ... ok test_add_1_3_and_5 (example.AddTestCase) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
注意,@parameterized.expand 通过在测试类上创建新方法来工作。如果第一个参数是字符串,该字符串将被添加到方法名称的末尾。例如,上面的测试用例将生成方法 test_add_0_2_and_3 和 test_add_1_3_and_5。
@parameterized.expand 生成的测试用例名称可以使用 name_func 关键字参数自定义。该值应该是一个接受三个参数的函数:testcase_func、param_num 和 params,并返回测试用例的名称。testcase_func 将是要测试的函数,param_num 将是测试用例参数在参数列表中的索引,而 param(param 的实例)将是要使用的参数。
import unittest
from parameterized import parameterized
def custom_name_func(testcase_func, param_num, param):
return "%s_%s" %(
testcase_func.__name__,
parameterized.to_safe_name("_".join(str(x) for x in param.args)),
)
class AddTestCase(unittest.TestCase):
@parameterized.expand([
(2, 3, 5),
(2, 3, 5),
], name_func=custom_name_func)
def test_add(self, a, b, expected):
assert_equal(a + b, expected)
将创建测试用例。
$ nosetests example.py test_add_1_2_3 (example.AddTestCase) ... ok test_add_2_3_5 (example.AddTestCase) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
param(...) 辅助类存储特定测试用例的参数。它可以用来向测试用例传递关键字参数。
from parameterized import parameterized, param
@parameterized([
param("10", 10),
param("10", 16, base=16),
])
def test_int(str_val, expected, base=10):
assert_equal(int(str_val, base=base), expected)
如果测试用例有文档字符串,该测试用例的参数将被追加到文档字符串的第一行。此行为可以用 doc_func 参数控制。
from parameterized import parameterized
@parameterized([
(1, 2, 3),
(4, 5, 9),
])
def test_add(a, b, expected):
""" Test addition. """
assert_equal(a + b, expected)
def my_doc_func(func, num, param):
return "%s: %s with %s" %(num, func.__name__, param)
@parameterized([
(5, 4, 1),
(9, 6, 3),
], doc_func=my_doc_func)
def test_subtraction(a, b, expected):
assert_equal(a - b, expected)
$ nosetests example.py Test addition. [with a=1, b=2, expected=3] ... ok Test addition. [with a=4, b=5, expected=9] ... ok 0: test_subtraction with param(*(5, 4, 1)) ... ok 1: test_subtraction with param(*(9, 6, 3)) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
最后,@parameterized_class 使用列表属性或应用于类的列表字典参数对整个类进行参数化。
from yourapp.models import User
from parameterized import parameterized_class
@parameterized_class([
{ "username": "user_1", "access_level": 1 },
{ "username": "user_2", "access_level": 2, "expected_status_code": 404 },
])
class TestUserAccessLevel(TestCase):
expected_status_code = 200
def setUp(self):
self.client.force_login(User.objects.get(username=self.username)[0])
def test_url_a(self):
response = self.client.get('/url')
self.assertEqual(response.status_code, self.expected_status_code)
def tearDown(self):
self.client.logout()
@parameterized_class(("username", "access_level", "expected_status_code"), [
("user_1", 1, 200),
("user_2", 2, 404)
])
class TestUserAccessLevel(TestCase):
def setUp(self):
self.client.force_login(User.objects.get(username=self.username)[0])
def test_url_a(self):
response = self.client.get("/url")
self.assertEqual(response.status_code, self.expected_status_code)
def tearDown(self):
self.client.logout()
@parameterized_class 装饰器接受一个 class_name_func 参数,用于控制 @parameterized_class 生成的参数化类的名称。
from parameterized import parameterized, parameterized_class
def get_class_name(cls, num, params_dict):
# By default the generated class named includes either the "name"
# parameter (if present), or the first string value. This example shows
# multiple parameters being included in the generated class name:
return "%s_%s_%s%s" %(
cls.__name__,
num,
parameterized.to_safe_name(params_dict['a']),
parameterized.to_safe_name(params_dict['b']),
)
@parameterized_class([
{ "a": "hello", "b": " world!", "expected": "hello world!" },
{ "a": "say ", "b": " cheese :)", "expected": "say cheese :)" },
], class_name_func=get_class_name)
class TestConcatenation(TestCase):
def test_concat(self):
self.assertEqual(self.a + self.b, self.expected)
$ nosetests -v test_math.py test_concat (test_concat.TestConcatenation_0_hello_world_) ... ok test_concat (test_concat.TestConcatenation_0_say_cheese__) ... ok
使用单个参数
如果测试函数只接受一个参数且该值不是可迭代的,则可以提供值列表,而无需将每个值包装在元组中。
@parameterized([1, 2, 3])
def test_greater_than_zero(value):
assert value > 0
然而,如果单个参数是可迭代的(如列表或元组),则必须将其包装在元组、列表或 param(...) 辅助类中。
@parameterized([
([1, 2, 3], ),
([3, 3], ),
([6], ),
])
def test_sums_to_6(numbers):
assert sum(numbers) == 6
注意,Python 要求单元素元组必须使用尾部逗号定义:(foo, )。
与 @mock.patch 一起使用
parameterized 可以与 mock.patch 一起使用,但参数顺序可能令人困惑。必须将 @mock.patch(...) 装饰器放在 @parameterized(...) 之下,并且模拟参数必须放在最后。
@mock.patch("os.getpid")
class TestOS(object):
@parameterized(...)
@mock.patch("os.fdopen")
@mock.patch("os.umask")
def test_method(self, param1, param2, ..., mock_umask, mock_fdopen, mock_getpid):
...
注意:当使用 @parameterized.expand 时,同样适用。
从 nose-parameterized 迁移到 parameterized
将代码库从 nose-parameterized 迁移到 parameterized
更新您的需求文件,将 nose-parameterized 替换为 parameterized。
将所有对 nose_parameterized 的引用替换为 parameterized
$ perl -pi -e 's/nose_parameterized/parameterized/g' your-codebase/
完成!
常见问题解答
- Python 2.X、3.5 和 3.6 的支持发生了什么变化?
从版本 0.9.0 开始,parameterized 不再支持 Python 2.X、3.5 或 3.6。之前的 parameterized 版本(最新的是 0.8.1)将继续工作,但不会接收任何新功能或错误修复。
- 当你说“nose 受最佳支持”时,是什么意思?
在 py.test 和 unittest 中有一些小的注意事项:py.test 不显示参数值(例如,它将显示 test_add[0] 而不是 test_add[1, 2, 3]),而 unittest/unittest2 不支持测试生成器,因此必须使用 @parameterized.expand。
- 为什么不使用 @pytest.mark.parametrize?
因为拼写很难。此外,parameterized 不需要你重复参数名称,并且(使用 param)它支持可选关键字参数。
- 为什么使用 @parameterized.expand 时会得到 AttributeError: 'function' object has no attribute 'expand' 错误?
你很可能安装了 parametrized(注意缺少的 e)。请使用 parameterized(带有 e)代替,你将一切就绪。
- nose-parameterized 发生了什么变化?
最初只支持 nose。但现在一切都被支持了,所以更改名称是合理的!
项目详情
parameterized-0.9.0.tar.gz 的散列值
算法 | 散列摘要 | |
---|---|---|
SHA256 | 7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1 |
|
MD5 | ed1bee2fb5d9044688d8503bdda9e6f3 |
|
BLAKE2b-256 | ea4900c0c0cc24ff4266025a53e41336b79adaa5a4ebfad214f433d623f9865e |