Написание метода повторного использования (параметризованного) 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.