2008-12-07 42 views
14

我正在對一系列函數進行單元測試,這些函數都有許多不變量。例如,用兩個矩陣調用函數會生成已知形狀的矩陣。如何在Python單元測試框架中簡潔地實現多個相似的單元測試?

我想寫單元測試來測試的此屬性功能整個家庭,而不必編寫單獨的測試的情況下爲每個功能(特別是由於多個功能可能稍後被添加)。要做到這一點

一種方法是遍歷這些功能的列表:

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」功能的測試用例,通過使用SETATTR用lamdba,但測試用例沒認出來。

我怎麼能改寫這個所以它仍然微不足道,擴大測試的名單,同時確保每個測試運行?

+0

相關的問題:http://stackoverflow.com/questions/32899/how-to-generate-dynamic-unit-tests-in-python – jfs 2008-12-07 20:46:14

回答

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

謝謝,這是有效的。只有一個輕微的怪癖,鼻子無法看到元類增加的測試用例。有什麼建議麼? – saffsd 2008-12-07 07:00:21

+0

我不熟悉鼻子。這增加了課程的方法,所以我不確定鼻子會錯過它們。不過,我很有興趣瞭解它的神奇之處。 – Dustin 2008-12-07 16:48:22

+0

在__new__中使用`f`可以很好地切割它,​​它有點模糊 – 2010-02-08 15:51:33

1

Metaclasses是一種選擇。另一種選擇是使用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

unittest.main()不會自動選擇它,並且它也不是。另外,FunctionTestCase調用沒有參數的setUp,並且functions_to_test需要被包裝在聲明測試的東西中。 – saffsd 2008-12-07 22:09:33

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 
1

上面元類代碼有麻煩,因爲鼻子鼻子在其selector.py wantMethod是LO在給定的測試方法__name__,而不是屬性代碼鍵。

要使用鼻子定義的元​​類定義測試方法,方法名稱和字典鍵必須相同,並且以鼻子(即'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 
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) 

一旦執行了TestCase1具有通過鼻子和單元測試都支持3種測試方法。

0

我讀過上面的例子元類的,我很喜歡它,但是它缺少兩樣東西:

  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)