跳转到主要内容

使用任何Python测试框架进行参数化测试

项目描述

PyPI PyPI - Downloads Circle CI

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_3test_add_1_3_and_5

@parameterized.expand 生成的测试用例名称可以使用 name_func 关键字参数自定义。该值应该是一个接受三个参数的函数:testcase_funcparam_numparams,并返回测试用例的名称。testcase_func 将是要测试的函数,param_num 将是测试用例参数在参数列表中的索引,而 paramparam 的实例)将是要使用的参数。

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

  1. 更新您的需求文件,将 nose-parameterized 替换为 parameterized

  2. 将所有对 nose_parameterized 的引用替换为 parameterized

    $ perl -pi -e 's/nose_parameterized/parameterized/g' your-codebase/
  3. 完成!

常见问题解答

Python 2.X、3.5 和 3.6 的支持发生了什么变化?

从版本 0.9.0 开始,parameterized 不再支持 Python 2.X、3.5 或 3.6。之前的 parameterized 版本(最新的是 0.8.1)将继续工作,但不会接收任何新功能或错误修复。

当你说“nose 受最佳支持”时,是什么意思?

py.testunittest 中有一些小的注意事项: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 (24.4 kB 查看散列值)

上传时间

构建分发

parameterized-0.9.0-py2.py3-none-any.whl (20.5 kB 查看散列值)

上传时间 Python 2 Python 3

由以下支持