Как кратко реализовать несколько аналогичных модульных тестов в рамочной системе Python unittest?

Я выполняю модульные тесты для семейства функций, которые все имеют ряд инвариантов. Например, вызов функции с помощью двух матриц создает матрицу известной формы.

Я хотел бы написать модульные тесты для тестирования всего семейства функций для этого свойства, без необходимости писать отдельный тестовый пример для каждой функции (в частности, поскольку больше функций может быть добавлено позже).

Один из способов сделать это – перебрать список этих функций:

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 . Там это не список проверяемых функций, а список известных пар «вход-выход». Проблема с этим подходом заключается в том, что если какой-либо элемент списка не прошел тест, более поздние элементы не проходят проверку.

Я посмотрел на subclassing unittest.TestCase и каким-то образом предоставил определенную функцию для проверки в качестве аргумента, но, насколько я могу судить, это мешает нам использовать unittest.main (), потому что не будет способа передать аргумент в тестовый файл.

Я также посмотрел на динамическое добавление функций «testSomething» в тестовую систему, используя setattr с помощью lamdba, но тестовая система не распознала их.

Как я могу это переписать, поэтому остается тривиально расширять список тестов, сохраняя при этом все тесты?

9 Solutions collect form web for “Как кратко реализовать несколько аналогичных модульных тестов в рамочной системе Python unittest?”

Вы можете использовать метакласс для динамической вставки тестов. Это отлично работает для меня:

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

Вот мой любимый подход к «семейству связанных тестов». Мне нравятся явные подклассы 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()

Если вы уже используете нос (и некоторые из ваших замечаний предлагают вам), почему бы вам просто не использовать тестовые генераторы , которые являются наиболее простым способом реализации параметрических тестов, с которыми я столкнулся:

Например:

 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 

Здесь вам не нужно использовать 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 метода тестирования, которые поддерживаются носом и unittest.

Я вижу, что этот вопрос старый. В то время я не уверен, но сегодня, может быть, вы можете использовать некоторые пакеты с «управляемыми данными»:

Метаклассы – один из вариантов. Другой вариант – использовать 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() 

Не тестировали. Но он должен работать нормально.

Вышеупомянутый код метакласса имеет проблемы с носом, потому что нос wantMethod в своем selector.py смотрит на имя __name__ данного метода тестирования, а не на атрибут dict key.

Чтобы использовать метод определения метакласса с носом, имя метода и ключ словаря должны быть одинаковыми и иметь префикс для обнаружения носом (т.е. с помощью «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 

Я прочитал приведенный выше пример метакласса, и мне это понравилось, но ему не хватало двух вещей:

  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) 

Проблема с этим подходом заключается в том, что если какой-либо элемент списка не прошел тест, более поздние элементы не проходят проверку.

Если вы посмотрите на это с точки зрения, что если тест не прошел, это критически важно, и весь пакет недействителен, то не имеет значения, что другие элементы не будут проверены, потому что «эй, у вас есть ошибка исправить'.

Как только это испытание пройдет, другие тесты будут выполняться.

По общему признанию, есть информация, которая может быть получена из знания о том, какие другие тесты терпят неудачу, и которые могут помочь в отладке, но, кроме того, предположить, что любой отказ теста – это полный сбой приложения.

Python - лучший язык программирования в мире.