Как кратко реализовать несколько аналогичных модульных тестов в рамочной системе 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. Там это не список проверяемых функций, а список известных пар "вход-выход". Проблема с этим подходом заключается в том, что если какой-либо элемент списка не прошел тест, более поздние элементы не проходят проверку.
Я посмотрел на подклассу unittest.TestCase и каким-то образом предоставил определенную функцию для проверки в качестве аргумента, но насколько я могу судить, это мешает нам использовать unittest.main(), потому что не будет способа передать аргумент тестовый файл.
Я также посмотрел на динамическое присоединение функций testSomething к тестовому регистру с помощью setattr с lamdba, но тестовая система не распознала их.
Как я могу это переписать, поэтому остается тривиально расширять список тестов, сохраняя при этом все тесты?
Ответы
Ответ 1
Вы можете использовать метакласс для динамической вставки тестов. Это отлично работает для меня:
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()
Ответ 2
Вот мой любимый подход к "семейству связанных тестов". Мне нравятся явные подклассы 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()
Ответ 3
Если вы уже используете нос (и некоторые из ваших замечаний предлагают вам), почему бы вам просто не использовать 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
Ответ 4
Вам не нужно использовать 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.
Ответ 5
Я вижу, что этот вопрос старый. В то время я не уверен, но сегодня, возможно, вы можете использовать некоторые "управляемые данными" пакеты:
Ответ 6
Метаклассы - один из вариантов. Другой вариант - использовать 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()
Не тестировались. Но он должен работать нормально.
Ответ 7
Вышеупомянутый код метакласса имеет проблемы с носом, потому что нос wantMethod в его selector.py смотрит на заданный метод __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
Ответ 8
Я прочитал приведенный выше пример метакласса, и мне понравилось, но ему не хватало двух вещей:
- Как управлять им с помощью структуры данных?
- Как убедиться, что тестовая функция написана правильно?
Я написал этот более полный пример, который управляется данными, и в котором тестовая функция сама тестируется на единицу.
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)
Ответ 9
Проблема с этим подходом заключается в том, что если какой-либо элемент списка тест, более поздние элементы не получают тестирование.
Если вы посмотрите на это с точки зрения, что, если тест завершился неудачно, это критически важно, и весь пакет недействителен, то не имеет значения, что другие элементы не будут проверены, потому что "эй, вы исправлена ошибка.
Как только этот тест пройдет, остальные тесты будут запущены.
По общему признанию, существует информация, полученная от знания о том, какие другие тесты терпят неудачу, и которые могут помочь в отладке, но, кроме того, предположить, что любой отказ теста - это полный сбой приложения.