简体   繁体   English

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

[英]How do I concisely implement multiple similar unit tests in the Python unittest framework?

I'm implementing unit tests for a family of functions that all share a number of invariants.我正在为所有共享许多不变量的函数族实施单元测试。 For example, calling the function with two matrices produce a matrix of known shape.例如,调用带有两个矩阵的函数会生成一个已知形状的矩阵。

I would like to write unit tests to test the entire family of functions for this property, without having to write an individual test case for each function (particularly since more functions might be added later).我想编写单元测试来测试此属性的整个函数系列,而不必为每个函数编写单独的测试用例(特别是因为以后可能会添加更多函数)。

One way to do this would be to iterate over a list of these functions:一种方法是迭代这些函数的列表:

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

I got the idea for this from Dive Into Python .我从Dive Into Python得到了这个想法。 There, it's not a list of functions being tested but a list of known input-output pairs.在那里,它不是正在测试的函数列表,而是已知输入输出对的列表。 The problem with this approach is that if any element of the list fails the test, the later elements don't get tested.这种方法的问题在于,如果列表中的任何元素未通过测试,后面的元素就不会得到测试。

I looked at subclassing unittest.TestCase and somehow providing the specific function to test as an argument, but as far as I can tell that prevents us from using unittest.main() because there would be no way to pass the argument to the testcase.我查看了子类化 unittest.TestCase 并以某种方式提供特定函数作为参数进行测试,但据我所知这会阻止我们使用 unittest.main() 因为没有办法将参数传递给测试用例。

I also looked at dynamically attaching "testSomething" functions to the testcase, by using setattr with a lamdba, but the testcase did not recognize them.我还查看了通过将 setattr 与 lamdba 一起使用,将“testSomething”函数动态附加到测试用例,但测试用例无法识别它们。

How can I rewrite this so it remains trivial to expand the list of tests, while still ensuring every test is run?我如何重写它,以便扩展测试列表仍然微不足道,同时仍确保运行每个测试?

Here's my favorite approach to the "family of related tests".这是我最喜欢的“相关测试系列”方法。 I like explicit subclasses of a TestCase that expresses the common features.我喜欢表达共同特征的 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 )

Add a function, add a subclass of MyTestF1 .添加一个函数,添加一个MyTestF1的子类。 Each subclass of MyTestF1 includes all of the tests in MyTestF1 with no duplicated code of any kind. MyTestF1 的每个子类都包括 MyTestF1 中的所有测试,没有任何类型的重复代码。

Unique features are handled in an obvious way.独特的功能以明显的方式处理。 New methods are added to the subclass.新方法被添加到子类中。

It's completely compatible with unittest.main()它与unittest.main()完全兼容

You don't have to use meta classes here.您不必在此处使用元类 A simple loop fits just fine.一个简单的循环就可以了。 Take a look at the example below:看看下面的例子:

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)

Once the for is executed, the TestCase1 has three test methods that are supported by both nose and unittest . for执行后,TestCase1 具有noseunittest都支持的三个测试方法。

You could use a metaclass to dynamically insert the tests.您可以使用元类来动态插入测试。 This works fine for me:这对我来说很好:

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

If you're already using nose (and some of your comments suggest you are), just use Test Generators , which are the most straightforward way to implement parametric tests I've come across:如果您已经在使用nose (并且您的一些评论表明您在使用),只需使用Test Generators ,这是实现我遇到的参数测试的最直接的方法:

For example:例如:

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

Produces:产生:

$ nosetests -d
...................
----------------------------------------------------------------------
Ran 19 tests in 0.009s

OK

The metaclass code in a previous answer has trouble with nose , because nose's wantMethod in its selector.py file is looking at a given test method's __name__ , not the attribute dict key.先前答案中的元类代码在使用nose时遇到问题,因为 nose 在其selector.py文件中的 wantMethod 正在查看给定测试方法的__name__ ,而不是属性 dict 键。

To use a metaclass defined test method with nose, the method name and dictionary key must be the same, and prefixed to be detected by nose (ie, with 'test_').要将元类定义的测试方法与 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

Metaclasses is one option.元类是一种选择。 Another option is to use a TestSuite :另一种选择是使用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()

Haven't tested.没有测试过。 But it should work fine.但它应该工作正常。

I've read the metaclass example, and I liked it, but it was missing two things:我读过元类示例,我喜欢它,但它缺少两件事:

  1. How to drive it with a data structure?如何用数据结构驱动它?
  2. How to make sure that the test function is written correctly?如何确保测试函数编写正确?

I wrote this more complete example, which is data-driven, and in which the test function is itself unit-tested.我写了这个更完整的例子,它是数据驱动的,其中测试函数本身是单元测试的。

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

When running the above example, if there's a bug in the code (or wrong test data), the error message will contain function name, which should help in debugging.在运行上面的例子时,如果代码中有错误(或错误的测试数据),错误消息将包含函数名称,这应该有助于调试。

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

The problem with this approach is that if any element of the list fails the test, the later elements don't get tested.这种方法的问题在于,如果列表中的任何元素未通过测试,后面的元素就不会得到测试。

If you look at it from the point of view that, if a test fails, that is critical and your entire package is invalid, then it doesn't matter that other elements won't get tested, because 'hey, you have an error to fix'.如果你从这样的角度来看,如果测试失败,那是很关键的,你的整个包是无效的,那么其他元素不会被测试并不重要,因为'嘿,你有一个错误修理'。

Once that test passes, the other tests will then run.一旦该测试通过,其他测试将运行。

Admittedly there is information to be gained from knowledge of which other tests are failing, and that can help with debugging, but apart from that, assume any test failure is an entire application failure.诚然,可以从其他测试失败的知识中获得信息,这有助于调试,但除此之外,假设任何测试失败都是整个应用程序失败。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM