你如何在python中生成动态(参数化)单元测试?

时间:2008-08-28 17:49:02

标签: python unit-testing parameterized-unit-test

我有一些测试数据,想为每个项目创建一个单元测试。我的第一个想法是这样做:

import unittest

l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]]

class TestSequence(unittest.TestCase):
    def testsample(self):
        for name, a,b in l:
            print "test", name
            self.assertEqual(a,b)

if __name__ == '__main__':
    unittest.main()

这样做的缺点是它在一次测试中处理所有数据。我想在运行中为每个项目生成一个测试。有什么建议吗?

24 个答案:

答案 0 :(得分:136)

我使用这样的东西:

import unittest

l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]]

class TestSequense(unittest.TestCase):
    pass

def test_generator(a, b):
    def test(self):
        self.assertEqual(a,b)
    return test

if __name__ == '__main__':
    for t in l:
        test_name = 'test_%s' % t[0]
        test = test_generator(t[1], t[2])
        setattr(TestSequense, test_name, test)
    unittest.main()

parameterized包可用于自动执行此过程:

from parameterized import parameterized

class TestSequence(unittest.TestCase):
    @parameterized.expand([
        ["foo", "a", "a",],
        ["bar", "a", "b"],
        ["lee", "b", "b"],
    ])
    def test_sequence(self, name, a, b):
        self.assertEqual(a,b)

将生成测试:

test_sequence_0_foo (__main__.TestSequence) ... ok
test_sequence_1_bar (__main__.TestSequence) ... FAIL
test_sequence_2_lee (__main__.TestSequence) ... ok

======================================================================
FAIL: test_sequence_1_bar (__main__.TestSequence)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/parameterized/parameterized.py", line 233, in <lambda>
    standalone_func = lambda *a: func(*(a + p.args), **p.kwargs)
  File "x.py", line 12, in test_sequence
    self.assertEqual(a,b)
AssertionError: 'a' != 'b'

答案 1 :(得分:102)

使用unittest(自3.4起)

自Python 3.4起,标准库unittest包具有subTest上下文管理器。

参见文档:

示例:

from unittest import TestCase

param_list = [('a', 'a'), ('a', 'b'), ('b', 'b')]

class TestDemonstrateSubtest(TestCase):
    def test_works_as_expected(self):
        for p1, p2 in param_list:
            with self.subTest():
                self.assertEqual(p1, p2)

您还可以为subTest()指定自定义消息和参数值:

with self.subTest(msg="Checking if p1 equals p2", p1=p1, p2=p2):

使用鼻子

nose测试框架supports this

示例(以下代码是包含测试的文件的全部内容):

param_list = [('a', 'a'), ('a', 'b'), ('b', 'b')]

def test_generator():
    for params in param_list:
        yield check_em, params[0], params[1]

def check_em(a, b):
    assert a == b

nosetests命令的输出:

> nosetests -v
testgen.test_generator('a', 'a') ... ok
testgen.test_generator('a', 'b') ... FAIL
testgen.test_generator('b', 'b') ... ok

======================================================================
FAIL: testgen.test_generator('a', 'b')
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python2.5/site-packages/nose-0.10.1-py2.5.egg/nose/case.py", line 203, in runTest
    self.test(*self.arg)
  File "testgen.py", line 7, in check_em
    assert a == b
AssertionError

----------------------------------------------------------------------
Ran 3 tests in 0.006s

FAILED (failures=1)

答案 2 :(得分:63)

这可以使用Metaclasses优雅地解决:

import unittest

l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]]

class TestSequenceMeta(type):
    def __new__(mcs, name, bases, dict):

        def gen_test(a, b):
            def test(self):
                self.assertEqual(a, b)
            return test

        for tname, a, b in l:
            test_name = "test_%s" % tname
            dict[test_name] = gen_test(a,b)
        return type.__new__(mcs, name, bases, dict)

class TestSequence(unittest.TestCase):
    __metaclass__ = TestSequenceMeta

if __name__ == '__main__':
    unittest.main()

答案 3 :(得分:42)

从Python 3.4开始,为了这个目的,已经将单测试引入了单元测试。有关详细信息,请参阅the documentation。 TestCase.subTest是一个上下文管理器,它允许在测试中隔离断言,以便使用参数信息报告失败但不会停止测试执行。以下是文档中的示例:

class NumbersTest(unittest.TestCase):

def test_even(self):
    """
    Test that numbers between 0 and 5 are all even.
    """
    for i in range(0, 6):
        with self.subTest(i=i):
            self.assertEqual(i % 2, 0)

测试运行的输出为:

======================================================================
FAIL: test_even (__main__.NumbersTest) (i=1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "subtests.py", line 32, in test_even
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

======================================================================
FAIL: test_even (__main__.NumbersTest) (i=3)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "subtests.py", line 32, in test_even
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

======================================================================
FAIL: test_even (__main__.NumbersTest) (i=5)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "subtests.py", line 32, in test_even
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

这也是unittest2的一部分,因此可用于早期版本的Python。

答案 4 :(得分:32)

load_tests是2.7中引入的一种鲜为人知的机制,用于动态创建TestSuite。有了它,您可以轻松创建参数化测试。

例如:

import unittest

class GeneralTestCase(unittest.TestCase):
    def __init__(self, methodName, param1=None, param2=None):
        super(GeneralTestCase, self).__init__(methodName)

        self.param1 = param1
        self.param2 = param2

    def runTest(self):
        pass  # Test that depends on param 1 and 2.


def load_tests(loader, tests, pattern):
    test_cases = unittest.TestSuite()
    for p1, p2 in [(1, 2), (3, 4)]:
        test_cases.addTest(GeneralTestCase('runTest', p1, p2))
    return test_cases

该代码将运行load_tests返回的TestSuite中的所有TestCase。发现机制不会自动运行其他测试。

或者,您也可以使用此票证中显示的继承:http://bugs.python.org/msg151444

答案 5 :(得分:27)

可以使用pytest来完成。只需使用内容编写文件test_me.py

import pytest

@pytest.mark.parametrize('name, left, right', [['foo', 'a', 'a'],
                                               ['bar', 'a', 'b'],
                                               ['baz', 'b', 'b']])
def test_me(name, left, right):
    assert left == right, name

使用命令py.test --tb=short test_me.py运行测试。然后输出将如下所示:

=========================== test session starts ============================
platform darwin -- Python 2.7.6 -- py-1.4.23 -- pytest-2.6.1
collected 3 items

test_me.py .F.

================================= FAILURES =================================
_____________________________ test_me[bar-a-b] _____________________________
test_me.py:8: in test_me
    assert left == right, name
E   AssertionError: bar
==================== 1 failed, 2 passed in 0.01 seconds ====================

很简单!此外,pytest还有更多功能,例如fixturesmarkassert等等。

答案 6 :(得分:9)

使用ddt库。它为测试方法添加了简单的装饰器:

import unittest
from ddt import ddt, data
from mycode import larger_than_two

@ddt
class FooTestCase(unittest.TestCase):

    @data(3, 4, 12, 23)
    def test_larger_than_two(self, value):
        self.assertTrue(larger_than_two(value))

    @data(1, -3, 2, 0)
    def test_not_larger_than_two(self, value):
        self.assertFalse(larger_than_two(value))

此库可以与pip一起安装。它不需要nose,并且在标准库unittest模块中运行良好。

答案 7 :(得分:6)

您可以通过试用TestScenarios库获益。

  

testscenarios为python unittest样式测试提供了干净的依赖注入。这可用于接口测试(通过单个测试套件测试许多实现)或经典依赖注入(在测试代码本身外部提供依赖性测试,允许在不同情况下轻松测试)。

答案 8 :(得分:5)

还有一些假设可以添加模糊或基于属性的测试:https://pypi.python.org/pypi/hypothesis

这是一种非常强大的测试方法。

答案 9 :(得分:4)

您可以使用nose-ittr插件(pip install nose-ittr)。

与现有测试集成非常容易,只需要很少的更改(如果有的话)。它还支持 nose 多处理插件。

并非每个测试都可以使用自定义setup功能。

@ittr(number=[1, 2, 3, 4])   
def test_even(self):   
    assert_equal(self.number % 2, 0)

还可以传递nosetest参数,例如内置插件attrib,这样您就可以只使用特定参数运行特定测试:

nosetest -a number=2

答案 10 :(得分:2)

我在查看ParamUnittestradon)的源代码时遇到了example usage on the github repo。它应该与扩展TestCase的其他框架(如Nose)一起使用。

以下是一个例子:

import unittest
import paramunittest


@paramunittest.parametrized(
    ('1', '2'),
    #(4, 3),    <---- uncomment to have a failing test
    ('2', '3'),
    (('4', ), {'b': '5'}),
    ((), {'a': 5, 'b': 6}),
    {'a': 5, 'b': 6},
)
class TestBar(TestCase):
    def setParameters(self, a, b):
        self.a = a
        self.b = b

    def testLess(self):
        self.assertLess(self.a, self.b)

答案 11 :(得分:2)

我使用元类和装饰器进行生成测试。您可以查看我的实施python_wrap_cases。该库不需要任何测试框架。

你的例子:

import unittest
from python_wrap_cases import wrap_case


@wrap_case
class TestSequence(unittest.TestCase):

    @wrap_case("foo", "a", "a")
    @wrap_case("bar", "a", "b")
    @wrap_case("lee", "b", "b")
    def testsample(self, name, a, b):
        print "test", name
        self.assertEqual(a, b)

控制台输出:

testsample_u'bar'_u'a'_u'b' (tests.example.test_stackoverflow.TestSequence) ... test bar
FAIL
testsample_u'foo'_u'a'_u'a' (tests.example.test_stackoverflow.TestSequence) ... test foo
ok
testsample_u'lee'_u'b'_u'b' (tests.example.test_stackoverflow.TestSequence) ... test lee
ok

您也可以使用生成器。例如,此代码生成具有参数a__listb__list

的所有可能的测试组合
import unittest
from python_wrap_cases import wrap_case


@wrap_case
class TestSequence(unittest.TestCase):

    @wrap_case(a__list=["a", "b"], b__list=["a", "b"])
    def testsample(self, a, b):
        self.assertEqual(a, b)

控制台输出:

testsample_a(u'a')_b(u'a') (tests.example.test_stackoverflow.TestSequence) ... ok
testsample_a(u'a')_b(u'b') (tests.example.test_stackoverflow.TestSequence) ... FAIL
testsample_a(u'b')_b(u'a') (tests.example.test_stackoverflow.TestSequence) ... FAIL
testsample_a(u'b')_b(u'b') (tests.example.test_stackoverflow.TestSequence) ... ok

答案 12 :(得分:1)

只需使用元类,如此处所示;

class DocTestMeta(type):
    """
    Test functions are generated in metaclass due to the way some
    test loaders work. For example, setupClass() won't get called
    unless there are other existing test methods, and will also
    prevent unit test loader logic being called before the test
    methods have been defined.
    """
    def __init__(self, name, bases, attrs):
        super(DocTestMeta, self).__init__(name, bases, attrs)

    def __new__(cls, name, bases, attrs):
        def func(self):
            """Inner test method goes here"""
            self.assertTrue(1)

        func.__name__ = 'test_sample'
        attrs[func.__name__] = func
        return super(DocTestMeta, cls).__new__(cls, name, bases, attrs)

class ExampleTestCase(TestCase):
    """Our example test case, with no methods defined"""
    __metaclass__ = DocTestMeta

输出:

test_sample (ExampleTestCase) ... OK

答案 13 :(得分:1)

您可以使用TestSuite和自定义TestCase课程。

import unittest

class CustomTest(unittest.TestCase):
    def __init__(self, name, a, b):
        super().__init__()
        self.name = name
        self.a = a
        self.b = b

    def runTest(self):
        print("test", self.name)
        self.assertEqual(self.a, self.b)

if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTest(CustomTest("Foo", 1337, 1337))
    suite.addTest(CustomTest("Bar", 0xDEAD, 0xC0DE))
    unittest.TextTestRunner().run(suite)

答案 14 :(得分:1)

import unittest

def generator(test_class, a, b):
    def test(self):
        self.assertEqual(a, b)
    return test

def add_test_methods(test_class):
    #First element of list is variable "a", then variable "b", then name of test case that will be used as suffix.
    test_list = [[2,3, 'one'], [5,5, 'two'], [0,0, 'three']]
    for case in test_list:
        test = generator(test_class, case[0], case[1])
        setattr(test_class, "test_%s" % case[2], test)


class TestAuto(unittest.TestCase):
    def setUp(self):
        print 'Setup'
        pass

    def tearDown(self):
        print 'TearDown'
        pass

_add_test_methods(TestAuto)  # It's better to start with underscore so it is not detected as a test itself

if __name__ == '__main__':
    unittest.main(verbosity=1)

结果:

>>> 
Setup
FTearDown
Setup
TearDown
.Setup
TearDown
.
======================================================================
FAIL: test_one (__main__.TestAuto)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:/inchowar/Desktop/PyTrash/test_auto_3.py", line 5, in test
    self.assertEqual(a, b)
AssertionError: 2 != 3

----------------------------------------------------------------------
Ran 3 tests in 0.019s

FAILED (failures=1)

答案 15 :(得分:0)

我在使用非常特殊的参数化测试方式时遇到了麻烦。我们所有的Selenium测试都可以在本地运行,但它们也应该能够在SauceLabs上的多个平台上远程运行。基本上,我想采用大量已编写的测试用例,并使用尽可能少的代码更改对它们进行参数化。此外,我需要能够将参数传递给setUp方法,这是我在其他地方没有看到的任何解决方案。

以下是我的想法:

import inspect
import types

test_platforms = [
    {'browserName': "internet explorer", 'platform': "Windows 7", 'version': "10.0"},
    {'browserName': "internet explorer", 'platform': "Windows 7", 'version': "11.0"},
    {'browserName': "firefox", 'platform': "Linux", 'version': "43.0"},
]


def sauce_labs():
    def wrapper(cls):
        return test_on_platforms(cls)
    return wrapper


def test_on_platforms(base_class):
    for name, function in inspect.getmembers(base_class, inspect.isfunction):
        if name.startswith('test_'):
            for platform in test_platforms:
                new_name = '_'.join(list([name, ''.join(platform['browserName'].title().split()), platform['version']]))
                new_function = types.FunctionType(function.__code__, function.__globals__, new_name,
                                                  function.__defaults__, function.__closure__)
                setattr(new_function, 'platform', platform)
                setattr(base_class, new_name, new_function)
            delattr(base_class, name)

    return base_class

有了这个,我所要做的就是为每个常规的旧TestCase添加一个简单的装饰器@sauce_labs(),现在运行它们时,它们被包装并重写,以便所有测试方法都被参数化并重命名。 LoginTests.test_login(self)以LoginTests.test_login_internet_explorer_10.0(self),LoginTests.test_login_internet_explorer_11.0(self)和LoginTests.test_login_firefox_43.0(self)运行,每个都有参数self.platform来决定什么浏览器/即使在LoginTests.setUp中也要运行的平台,这对我的任务至关重要,因为那里初始化了与SauceLabs的连接。

无论如何,我希望这对那些希望对他们的测试进行类似的“全局”参数化的人有所帮助!

答案 16 :(得分:0)

此解决方案适用于unittestnose

#!/usr/bin/env python
import unittest

def make_function(description, a, b):
    def ghost(self):
        self.assertEqual(a, b, description)
    print description
    ghost.__name__ = 'test_{0}'.format(description)
    return ghost


class TestsContainer(unittest.TestCase):
    pass

testsmap = {
    'foo': [1, 1],
    'bar': [1, 2],
    'baz': [5, 5]}

def generator():
    for name, params in testsmap.iteritems():
        test_func = make_function(name, params[0], params[1])
        setattr(TestsContainer, 'test_{0}'.format(name), test_func)

generator()

if __name__ == '__main__':
    unittest.main()

答案 17 :(得分:0)

元编程很有趣,但可以开始。这里的大多数解决方案都难以:

  • 有选择地启动测试
  • 回到给出测试名称的代码

所以,我的第一个建议是遵循简单/显式路径(适用于任何测试运行器):

import unittest

class TestSequence(unittest.TestCase):

    def _test_complex_property(self, a, b):
        self.assertEqual(a,b)

    def test_foo(self):
        self._test_complex_property("a", "a")
    def test_bar(self):
        self._test_complex_property("a", "b")
    def test_lee(self):
        self._test_complex_property("b", "b")

if __name__ == '__main__':
    unittest.main()

由于我们不应该重复自己,我的第二个建议建立在@Javier的答案之上:拥抱基于属性的测试。假设库:

  • 对于测试用例生成而言比我们仅仅是人类更加无情地嘲笑&#34;
  • 将提供简单的count-examples
  • 适用于任何测试跑步者
  • 有许多有趣的功能(统计数据,额外的测试输出......)

    class TestSequence(unittest.TestCase):

    @given(st.text(), st.text())
    def test_complex_property(self, a, b):
        self.assertEqual(a,b)
    

要测试您的具体示例,只需添加:

    @example("a", "a")
    @example("a", "b")
    @example("b", "b")

要仅运行一个特定示例,您可以注释掉其他示例(提供的示例将首先运行)。您可能想要使用@given(st.nothing())。另一个选择是通过以下方式替换整个块:

    @given(st.just("a"), st.just("b"))

好的,你没有不同的测试名称。但也许你只需要:

  • 被测财产的描述性名称。
  • 哪个输入导致失败(伪造示例)。

Funnier example

答案 18 :(得分:0)

参加派对的时间已经很晚了,但我在为<div class='row pb-3' ng-repeat='blog in $ctrl.blogs track by $index'> ... <div class='col-12'> <a class='float-right view-link' data-toggle='collapse' href="#commentDiv{{$index}}" role="button" aria-expanded="false" aria-controls="commentDiv{{$index}}"><u><span class='h4'><i class="fa fa-comments" aria-hidden="true"></i> View Comments</span></u></a> </div><!--End of col-12--> ... <div class="collapse" id="commentDiv{{$index}}"> <div class="card card-body" ng-repeat='comment in blog.comments'> Some text <p>{{comment.username}}</p> </div><!--End of card-body--> </div><!--End of commentDiv--> 进行这些工作时遇到了麻烦。

这是@Javier's answer的一个版本,可以setUpClass访问动态分配的属性。

setUpClass

输出

import unittest


class GeneralTestCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print ''
        print cls.p1
        print cls.p2

    def runTest1(self):
        self.assertTrue((self.p2 - self.p1) == 1)

    def runTest2(self):
        self.assertFalse((self.p2 - self.p1) == 2)


def load_tests(loader, tests, pattern):
    test_cases = unittest.TestSuite()
    for p1, p2 in [(1, 2), (3, 4)]:
        clsname = 'TestCase_{}_{}'.format(p1, p2)
        dct = {
            'p1': p1,
            'p2': p2,
        }
        cls = type(clsname, (GeneralTestCase,), dct)
        test_cases.addTest(cls('runTest1'))
        test_cases.addTest(cls('runTest2'))
    return test_cases

答案 19 :(得分:0)

我发现这很适合我的目的,尤其是当我需要生成对数据集合的过程做些微改动的测试时。

import unittest

def rename(newName):
    def renamingFunc(func):
        func.__name__ == newName
        return func
    return renamingFunc

class TestGenerator(unittest.TestCase):

    TEST_DATA = {}

    @classmethod
    def generateTests(cls):
        for dataName, dataValue in TestGenerator.TEST_DATA:
            for func in cls.getTests(dataName, dataValue):
                setattr(cls, "test_{:s}_{:s}".format(func.__name__, dataName), func)

    @classmethod
    def getTests(cls):
        raise(NotImplementedError("This must be implemented"))

class TestCluster(TestGenerator):

    TEST_CASES = []

    @staticmethod
    def getTests(dataName, dataValue):

        def makeTest(case):

            @rename("{:s}".format(case["name"]))
            def test(self):
                # Do things with self, case, data
                pass

            return test

        return [makeTest(c) for c in TestCluster.TEST_CASES]

TestCluster.generateTests()

TestGenerator类可用于生成不同的测试用例集,例如TestCluster

TestCluster可以看作是TestGenerator接口的实现。

答案 20 :(得分:0)

导入unittest

def生成器(test_class,a,b,c,d,name):     def测试(自己):         打印('Testexecution =',名称)         打印('a =',a)         打印('b =',b)         打印('c =',c)         print('d =',d)

return test

def add_test_methods(test_class):     test_list = [[3,3,5,6,'一个'],[5,5,8,9,'两个'],[0,0,5,6,'三个'],[0,0, 2,3,'四个']]     对于test_list中的情况:         打印('case =',case [0],case [1],case [2],case [3],case [4])         测试=生成器(test_class,案例[0],案例[1],案例[2],案例[3],案例[4])         setattr(test_class,“ test_%s”%case [4],测试)

Class TestAuto(unittest.TestCase):     def setUp():         打印设置')         通过

def tearDown(self):
    print ('TearDown')
    pass

add_test_methods(TestAuto)

如果名称 =='主要”:     unittest.main(verbosity = 1)

答案 21 :(得分:0)

只需在混合中添加其他解决方案;)

这实际上与上述parameterized相同,但特定于unittest

def sub_test(param_list):
    """Decorates a test case to run it as a set of subtests."""

    def decorator(f):

        @functools.wraps(f)
        def wrapped(self):
            for param in param_list:
                with self.subTest(**param):
                    f(self, **param)

        return wrapped

    return decorator

用法示例:

class TestStuff(unittest.TestCase):
    @sub_test([
        dict(arg1='a', arg2='b'),
        dict(arg1='x', arg2='y'),
    ])
    def test_stuff(self, a, b):
        ...

答案 22 :(得分:-1)

除了使用setattr之外,我们可以使用自python 3.2以来的load_tests。请参阅博客文章blog.livreuro.com/en/coding/python/how-to-generate-discoverable-unit-tests-in-python-dynamically/

class Test(unittest.TestCase):
    pass

def _test(self, file_name):
    open(file_name, 'r') as f:
        self.assertEqual('test result',f.read())

def _generate_test(file_name):
    def test(self):
        _test(self, file_name)
    return test

def _generate_tests():
    for file in files:
        file_name = os.path.splitext(os.path.basename(file))[0]
        setattr(Test, 'test_%s' % file_name, _generate_test(file))

test_cases = (Test,)

def load_tests(loader, tests, pattern):
    _generate_tests()
    suite = TestSuite()
    for test_class in test_cases:
        tests = loader.loadTestsFromTestCase(test_class)
        suite.addTests(tests)
    return suite

if __name__ == '__main__':
    _generate_tests()
    unittest.main()

答案 23 :(得分:-1)

以下是我的解决方案。我发现这有用的时候: 1.应该为unittest.Testcase和unittest发现 2.为不同的参数设置运行一组测试。 3.非常简单,不依赖于其他包         import unittest

    class BaseClass(unittest.TestCase):
        def setUp(self):
            self.param = 2
            self.base = 2

        def test_me(self):
            self.assertGreaterEqual(5, self.param+self.base)

        def test_me_too(self):
            self.assertLessEqual(3, self.param+self.base)



     class Child_One(BaseClass):
        def setUp(self):
            BaseClass.setUp(self)
            self.param = 4


     class Child_Two(BaseClass):
        def setUp(self):
            BaseClass.setUp(self)
            self.param = 1