簡體   English   中英

如何在Python unittest框架中簡潔的實現多個相似的單元測試?

[英]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)

for執行后,TestCase1 具有noseunittest都支持的三個測試方法。

您可以使用元類來動態插入測試。 這對我來說很好:

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

沒有測試過。 但它應該工作正常。

我讀過元類示例,我喜歡它,但它缺少兩件事:

  1. 如何用數據結構驅動它?
  2. 如何確保測試函數編寫正確?

我寫了這個更完整的例子,它是數據驅動的,其中測試函數本身是單元測試的。

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM