如何在Python unittest框架中简明地实现多个类似的单元测试?

时间:2008-12-07 01:59:50

标签: python unit-testing

我正在为一系列函数实现单元测试,这些函数都共享多个不变量。例如,用两个矩阵调用函数会产生一个已知形状的矩阵。

我想编写单元测试来测试此属性的整个函数系列,而不必为每个函数编写单独的测试用例(特别是因为稍后可能会添加更多函数)。

执行此操作的一种方法是迭代这些函数的列表:

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()因为没有办法将参数传递给测试用例。

我还看了动态地将“testSomething”函数附加到测试用例,使用带有lamdba的setattr,但是测试用例没有识别它们。

如何重写这一点,以便扩展测试列表仍然微不足道,同时仍然确保每个测试都运行?

9 个答案:

答案 0 :(得分:11)

这是我最喜欢的“相关测试系列”的方法。我喜欢表达常见功能的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()

完全兼容

答案 1 :(得分:5)

如果您已经在使用鼻子(并且您的一些评论表明您是这样),那么为什么不使用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

答案 2 :(得分:5)

您不必在此处使用Meta Classes。一个简单的循环就可以了。看一下下面的例子:

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)

执行for后,TestCase1有3种测试方法,由鼻子和单元测试支持。

答案 3 :(得分:4)

您可以使用元类来动态插入测试。这对我来说很好:

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()

答案 4 :(得分:3)

我看到这个问题已经过时了。我当时不确定,但今天也许你可以使用一些“数据驱动的测试”软件包:

答案 5 :(得分:1)

元类是一种选择。另一种选择是使用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()

尚未测试过。但它应该可以正常工作。

答案 6 :(得分:1)

上面的元类代码有鼻子的问题,因为鼻子在其selector.py中的wantMethod正在查看给定的测试方法的__name__,而不是属性dict键。

要使用带有鼻子的元类定义测试方法,方法名称和字典键必须相同,并且前缀为鼻子检测(即使用'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

答案 7 :(得分:0)

我已经阅读了上面的元类示例,我很喜欢它,但它缺少两件事:

  1. 如何使用数据结构驱动它?
  2. 如何确保正确编写测试功能?
  3. 我写了这个更完整的例子,它是数据驱动的,其中测试函数本身是单元测试的。

    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)
    

答案 8 :(得分:-1)

  

这种方法的问题在于   如果列表中的任何元素未通过   测试,后来的元素没有得到   测试

如果你从一个角度来看,如果一个测试失败,这是至关重要的,你的整个包都无效,那么其他元素不会被测试也没关系,因为'嘿,你有错误要修复'。

一旦测试通过,其他测试就会运行。

不可否认,从其他测试失败的知识中可以获得信息,这可以帮助调试,但除此之外,假设任何测试失败都是整个应用程序失败。