[英]How do I structure my tests with the Python 'unittest' module?
[英]How do I concisely implement multiple similar unit tests in the Python unittest framework?
我正在为所有共享许多不变量的函数族实施单元测试。 例如,调用带有两个矩阵的函数会生成一个已知形状的矩阵。
我想编写单元测试来测试此属性的整个函数系列,而不必为每个函数编写单独的测试用例(特别是因为以后可能会添加更多函数)。
一种方法是迭代这些函数的列表:
import unittest
import numpy
from somewhere import the_functions
from somewhere.else import TheClass
class Test_the_functions(unittest.TestCase):
def setUp(self):
self.matrix1 = numpy.ones((5,10))
self.matrix2 = numpy.identity(5)
def testOutputShape(unittest.TestCase):
"""Output of functions be of a certain shape"""
for function in all_functions:
output = function(self.matrix1, self.matrix2)
fail_message = "%s produces output of the wrong shape" % str(function)
self.assertEqual(self.matrix1.shape, output.shape, fail_message)
if __name__ == "__main__":
unittest.main()
我从Dive Into Python得到了这个想法。 在那里,它不是正在测试的函数列表,而是已知输入输出对的列表。 这种方法的问题在于,如果列表中的任何元素未通过测试,后面的元素就不会得到测试。
我查看了子类化 unittest.TestCase 并以某种方式提供特定函数作为参数进行测试,但据我所知这会阻止我们使用 unittest.main() 因为没有办法将参数传递给测试用例。
我还查看了通过将 setattr 与 lamdba 一起使用,将“testSomething”函数动态附加到测试用例,但测试用例无法识别它们。
我如何重写它,以便扩展测试列表仍然微不足道,同时仍确保运行每个测试?
这是我最喜欢的“相关测试系列”方法。 我喜欢表达共同特征的 TestCase 的显式子类。
class MyTestF1( unittest.TestCase ):
theFunction= staticmethod( f1 )
def setUp(self):
self.matrix1 = numpy.ones((5,10))
self.matrix2 = numpy.identity(5)
def testOutputShape( self ):
"""Output of functions be of a certain shape"""
output = self.theFunction(self.matrix1, self.matrix2)
fail_message = "%s produces output of the wrong shape" % (self.theFunction.__name__,)
self.assertEqual(self.matrix1.shape, output.shape, fail_message)
class TestF2( MyTestF1 ):
"""Includes ALL of TestF1 tests, plus a new test."""
theFunction= staticmethod( f2 )
def testUniqueFeature( self ):
# blah blah blah
pass
class TestF3( MyTestF1 ):
"""Includes ALL of TestF1 tests with no additional code."""
theFunction= staticmethod( f3 )
添加一个函数,添加一个MyTestF1
的子类。 MyTestF1 的每个子类都包括 MyTestF1 中的所有测试,没有任何类型的重复代码。
独特的功能以明显的方式处理。 新方法被添加到子类中。
它与unittest.main()
完全兼容
您不必在此处使用元类。 一个简单的循环就可以了。 看看下面的例子:
import unittest
class TestCase1(unittest.TestCase):
def check_something(self, param1):
self.assertTrue(param1)
def _add_test(name, param1):
def test_method(self):
self.check_something(param1)
setattr(TestCase1, 'test_' + name, test_method)
test_method.__name__ = 'test_' + name
for i in range(0, 3):
_add_test(str(i), False)
您可以使用元类来动态插入测试。 这对我来说很好:
import unittest
class UnderTest(object):
def f1(self, i):
return i + 1
def f2(self, i):
return i + 2
class TestMeta(type):
def __new__(cls, name, bases, attrs):
funcs = [t for t in dir(UnderTest) if t[0] == 'f']
def doTest(t):
def f(slf):
ut=UnderTest()
getattr(ut, t)(3)
return f
for f in funcs:
attrs['test_gen_' + f] = doTest(f)
return type.__new__(cls, name, bases, attrs)
class T(unittest.TestCase):
__metaclass__ = TestMeta
def testOne(self):
self.assertTrue(True)
if __name__ == '__main__':
unittest.main()
如果您已经在使用nose (并且您的一些评论表明您在使用),只需使用Test Generators ,这是实现我遇到的参数测试的最直接的方法:
例如:
from binary_search import search1 as search
def test_binary_search():
data = (
(-1, 3, []),
(-1, 3, [1]),
(0, 1, [1]),
(0, 1, [1, 3, 5]),
(1, 3, [1, 3, 5]),
(2, 5, [1, 3, 5]),
(-1, 0, [1, 3, 5]),
(-1, 2, [1, 3, 5]),
(-1, 4, [1, 3, 5]),
(-1, 6, [1, 3, 5]),
(0, 1, [1, 3, 5, 7]),
(1, 3, [1, 3, 5, 7]),
(2, 5, [1, 3, 5, 7]),
(3, 7, [1, 3, 5, 7]),
(-1, 0, [1, 3, 5, 7]),
(-1, 2, [1, 3, 5, 7]),
(-1, 4, [1, 3, 5, 7]),
(-1, 6, [1, 3, 5, 7]),
(-1, 8, [1, 3, 5, 7]),
)
for result, n, ns in data:
yield check_binary_search, result, n, ns
def check_binary_search(expected, n, ns):
actual = search(n, ns)
assert expected == actual
产生:
$ nosetests -d
...................
----------------------------------------------------------------------
Ran 19 tests in 0.009s
OK
您可以使用一些“数据驱动测试”包:
先前答案中的元类代码在使用nose时遇到问题,因为 nose 在其selector.py文件中的 wantMethod 正在查看给定测试方法的__name__
,而不是属性 dict 键。
要将元类定义的测试方法与 nose 一起使用,方法名称和字典键必须相同,并添加前缀以供 nose 检测(即,带有'test_')。
# Test class that uses a metaclass
class TCType(type):
def __new__(cls, name, bases, dct):
def generate_test_method():
def test_method(self):
pass
return test_method
dct['test_method'] = generate_test_method()
return type.__new__(cls, name, bases, dct)
class TestMetaclassed(object):
__metaclass__ = TCType
def test_one(self):
pass
def test_two(self):
pass
元类是一种选择。 另一种选择是使用TestSuite
:
import unittest
import numpy
import funcs
# get references to functions
# only the functions and if their names start with "matrixOp"
functions_to_test = [v for k,v in funcs.__dict__ if v.func_name.startswith('matrixOp')]
# suplly an optional setup function
def setUp(self):
self.matrix1 = numpy.ones((5,10))
self.matrix2 = numpy.identity(5)
# create tests from functions directly and store those TestCases in a TestSuite
test_suite = unittest.TestSuite([unittest.FunctionTestCase(f, setUp=setUp) for f in functions_to_test])
if __name__ == "__main__":
unittest.main()
没有测试过。 但它应该工作正常。
我读过元类示例,我喜欢它,但它缺少两件事:
我写了这个更完整的例子,它是数据驱动的,其中测试函数本身是单元测试的。
import unittest
TEST_DATA = (
(0, 1),
(1, 2),
(2, 3),
(3, 5), # This intentionally written to fail
)
class Foo(object):
def f(self, n):
return n + 1
class FooTestBase(object):
"""Base class, defines a function which performs assertions.
It defines a value-driven check, which is written as a typical function, and
can be tested.
"""
def setUp(self):
self.obj = Foo()
def value_driven_test(self, number, expected):
self.assertEquals(expected, self.obj.f(number))
class FooTestBaseTest(unittest.TestCase):
"""FooTestBase has a potentially complicated, data-driven function.
It needs to be tested.
"""
class FooTestExample(FooTestBase, unittest.TestCase):
def runTest(self):
return self.value_driven_test
def test_value_driven_test_pass(self):
test_base = self.FooTestExample()
test_base.setUp()
test_base.value_driven_test(1, 2)
def test_value_driven_test_fail(self):
test_base = self.FooTestExample()
test_base.setUp()
self.assertRaises(
AssertionError,
test_base.value_driven_test, 1, 3)
class DynamicTestMethodGenerator(type):
"""Class responsible for generating dynamic test functions.
It only wraps parameters for specific calls of value_driven_test. It could
be called a form of currying.
"""
def __new__(cls, name, bases, dct):
def generate_test_method(number, expected):
def test_method(self):
self.value_driven_test(number, expected)
return test_method
for number, expected in TEST_DATA:
method_name = "testNumbers_%s_and_%s" % (number, expected)
dct[method_name] = generate_test_method(number, expected)
return type.__new__(cls, name, bases, dct)
class FooUnitTest(FooTestBase, unittest.TestCase):
"""Combines generated and hand-written functions."""
__metaclass__ = DynamicTestMethodGenerator
if __name__ == '__main__':
unittest.main()
在运行上面的例子时,如果代码中有错误(或错误的测试数据),错误消息将包含函数名称,这应该有助于调试。
.....F
======================================================================
FAIL: testNumbers_3_and_5 (__main__.FooUnitTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "dyn_unittest.py", line 65, in test_method
self.value_driven_test(number, expected)
File "dyn_unittest.py", line 30, in value_driven_test
self.assertEquals(expected, self.obj.f(number))
AssertionError: 5 != 4
----------------------------------------------------------------------
Ran 6 tests in 0.002s
FAILED (failures=1)
这种方法的问题在于,如果列表中的任何元素未通过测试,后面的元素就不会得到测试。
如果你从这样的角度来看,如果测试失败,那是很关键的,你的整个包是无效的,那么其他元素不会被测试并不重要,因为'嘿,你有一个错误修理'。
一旦该测试通过,其他测试将运行。
诚然,可以从其他测试失败的知识中获得信息,这有助于调试,但除此之外,假设任何测试失败都是整个应用程序失败。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.