Написание метода повторного использования (параметризованного) unittest.TestCase
Возможный дубликат:
Как создать динамические (параметризованные) модульные тесты в python?
Я пишу тесты с помощью пакета unittest, и я хочу избежать повторного кода. Я собираюсь выполнить ряд тестов, для которых требуется очень похожий метод, но каждый раз с одним значением. Простой и бесполезный пример:
class ExampleTestCase(unittest.TestCase):
def test_1(self):
self.assertEqual(self.somevalue, 1)
def test_2(self):
self.assertEqual(self.somevalue, 2)
def test_3(self):
self.assertEqual(self.somevalue, 3)
def test_4(self):
self.assertEqual(self.somevalue, 4)
Есть ли способ записать вышеприведенный пример без повторения всего кода каждый раз, но вместо этого писать общий метод, например
def test_n(self, n):
self.assertEqual(self.somevalue, n)
и сообщить unittest, чтобы попробовать этот тест с разными входами?
Ответы
Ответ 1
Некоторые из инструментов, доступных для выполнения параметризованных тестов в Python:
Ответ 2
Если вы действительно хотите иметь несколько unitttest, вам нужно несколько методов. Единственный способ добиться этого - это генерировать код. Вы можете сделать это через метаклассы или путем настройки класса после определения, включая (если вы используете Python 2.6) через декоратор класса.
Здесь предлагается решение, которое ищет специальные "мультитесты" и "многоэлементные_значения" и использует их для создания методов тестирования "на лету". Не элегантный, но он делает примерно то, что вы хотите:
import unittest
import inspect
class SomeValue(object):
def __eq__(self, other):
return other in [1, 3, 4]
class ExampleTestCase(unittest.TestCase):
somevalue = SomeValue()
multitest_values = [1, 2, 3, 4]
def multitest(self, n):
self.assertEqual(self.somevalue, n)
multitest_gt_values = "ABCDEF"
def multitest_gt(self, c):
self.assertTrue(c > "B", c)
def add_test_cases(cls):
values = {}
functions = {}
# Find all the 'multitest*' functions and
# matching list of test values.
for key, value in inspect.getmembers(cls):
if key.startswith("multitest"):
if key.endswith("_values"):
values[key[:-7]] = value
else:
functions[key] = value
# Put them together to make a list of new test functions.
# One test function for each value
for key in functions:
if key in values:
function = functions[key]
for i, value in enumerate(values[key]):
def test_function(self, function=function, value=value):
function(self, value)
name ="test%s_%d" % (key[9:], i+1)
test_function.__name__ = name
setattr(cls, name, test_function)
add_test_cases(ExampleTestCase)
if __name__ == "__main__":
unittest.main()
Это результат, когда я запускаю его
% python stackoverflow.py
.F..FF....
======================================================================
FAIL: test_2 (__main__.ExampleTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stackoverflow.py", line 34, in test_function
function(self, value)
File "stackoverflow.py", line 13, in multitest
self.assertEqual(self.somevalue, n)
AssertionError: <__main__.SomeValue object at 0xd9870> != 2
======================================================================
FAIL: test_gt_1 (__main__.ExampleTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stackoverflow.py", line 34, in test_function
function(self, value)
File "stackoverflow.py", line 17, in multitest_gt
self.assertTrue(c > "B", c)
AssertionError: A
======================================================================
FAIL: test_gt_2 (__main__.ExampleTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stackoverflow.py", line 34, in test_function
function(self, value)
File "stackoverflow.py", line 17, in multitest_gt
self.assertTrue(c > "B", c)
AssertionError: B
----------------------------------------------------------------------
Ran 10 tests in 0.001s
FAILED (failures=3)
Вы можете сразу увидеть некоторые проблемы, возникающие при генерации кода. Откуда происходит "test_gt_1"? Я мог бы изменить имя на более длинный "test_multitest_gt_1", но затем какой тест равен 1? Лучше здесь начинать с _0 вместо _1, и, возможно, в вашем случае вы знаете, что значения могут использоваться как имя функции Python.
Мне не нравится этот подход. Я работал над базами кода, которые автоматически генерировали методы тестирования (в одном случае с использованием метакласса), и оказалось, что было намного сложнее понять, чем это было полезно. Когда тест не удался, было сложно определить источник отказа, и было сложно вставить код отладки, чтобы выяснить причину сбоя.
(Ошибки отладки в примере, который я написал здесь, не так сложны, как этот метаклассический подход, с которым мне приходилось работать.)
Ответ 3
Я предполагаю, что вы хотите "параметризованные тесты".
Я не думаю, что модуль unittest поддерживает это (к сожалению)
но если бы я добавил эту функцию, это выглядело бы примерно так:
# Will run the test for all combinations of parameters
@RunTestWith(x=[0, 1, 2, 3], y=[-1, 0, 1])
def testMultiplication(self, x, y):
self.assertEqual(multiplication.multiply(x, y), x*y)
С существующим модулем unittest простой декоратор, подобный этому, не сможет "реплицировать" тест несколько раз, но я думаю, что это выполнимо, используя комбинацию декоратора и метакласса (metaclass должен соблюдать все "тесты" * 'и реплицировать (под разными автогенерируемыми именами) те, которые применяются декоратором).
Ответ 4
Более ориентированный на данные подход может быть более понятным, чем тот, который используется в Andrew Dalke answer
"""Parametrized unit test.
Builds a single TestCase class which tests if its
`somevalue` method is equal to the numbers 1 through 4.
This is accomplished by
creating a list (`cases`)
of dictionaries which contain test specifications
and then feeding the list to a function which creates a test case class.
When run, the output shows that three of the four cases fail,
as expected:
>>> import sys
>>> from unittest import TextTestRunner
>>> run_tests(TextTestRunner(stream=sys.stdout, verbosity=9))
... # doctest: +ELLIPSIS
Test if self.somevalue equals 4 ... FAIL
Test if self.somevalue equals 1 ... FAIL
Test if self.somevalue equals 3 ... FAIL
Test if self.somevalue equals 2 ... ok
<BLANKLINE>
======================================================================
FAIL: Test if self.somevalue equals 4
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: 2 != 4
<BLANKLINE>
======================================================================
FAIL: Test if self.somevalue equals 1
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: 2 != 1
<BLANKLINE>
======================================================================
FAIL: Test if self.somevalue equals 3
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: 2 != 3
<BLANKLINE>
----------------------------------------------------------------------
Ran 4 tests in ...s
<BLANKLINE>
FAILED (failures=3)
"""
from unittest import TestCase, TestSuite, defaultTestLoader
cases = [{'name': "somevalue_equals_one",
'doc': "Test if self.somevalue equals 1",
'value': 1},
{'name': "somevalue_equals_two",
'doc': "Test if self.somevalue equals 2",
'value': 2},
{'name': "somevalue_equals_three",
'doc': "Test if self.somevalue equals 3",
'value': 3},
{'name': "somevalue_equals_four",
'doc': "Test if self.somevalue equals 4",
'value': 4}]
class BaseTestCase(TestCase):
def setUp(self):
self.somevalue = 2
def test_n(self, n):
self.assertEqual(self.somevalue, n)
def make_parametrized_testcase(class_name, base_classes, test_method, cases):
def make_parametrized_test_method(name, value, doc=None):
def method(self):
return test_method(self, value)
method.__name__ = "test_" + name
method.__doc__ = doc
return (method.__name__, method)
test_methods = (make_parametrized_test_method(**case) for case in cases)
class_dict = dict(test_methods)
return type(class_name, base_classes, class_dict)
TestCase = make_parametrized_testcase('TestOneThroughFour',
(BaseTestCase,),
test_n,
cases)
def make_test_suite():
load = defaultTestLoader.loadTestsFromTestCase
return TestSuite(load(TestCase))
def run_tests(runner):
runner.run(make_test_suite())
if __name__ == '__main__':
from unittest import TextTestRunner
run_tests(TextTestRunner(verbosity=9))
Я не уверен, что вуду участвует в определении порядка, в котором выполняются тесты, но доктрины проходят для меня, по крайней мере, для меня.
В более сложных ситуациях можно заменить элемент values
словарей cases
на кортеж, содержащий список аргументов и аргумент ключевых слов. Хотя в этот момент вы в основном кодируете lisp в python.
Ответ 5
Возможно, что-то вроде:
def test_many(self):
for n in range(0,1000):
self.assertEqual(self.somevalue, n)
Ответ 6
Напишите один тестовый метод, который выполняет все ваши тесты и фиксирует все результаты, записывает ваши собственные диагностические сообщения в stderr и не проверяет, не срабатывает ли какое-либо из его подтестов:
def test_with_multiple_parameters(self):
failed = False
for k in sorted(self.test_parameters.keys()):
if not self.my_test(self.test_parameters[k]):
print >> sys.stderr, "Test {0} failed.".format(k)
failed = True
self.assertFalse(failed)
Обратите внимание, что, конечно, имя my_test()
не может начинаться с test
.