Написание повторно используемого (параметризованного) метода 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 попробовать этот тест с разными входами?


person astrofrog    schedule 04.11.2009    source источник
comment
Вы нашли способ сделать это? Или, может быть, вы нашли другой инструмент для этой задачи? Мне тоже нужно такое поведение.   -  person legesh    schedule 11.11.2010
comment
thebongtraveller.blogspot.sg/2015/12/ Это одно и то же?   -  person Arindam Roychowdhury    schedule 29.11.2016


Ответы (6)


Вот некоторые из инструментов, доступных для параметризованных тестов в Python:

person akaihola    schedule 05.10.2012
comment
Параметризованные тестовые примеры, Эли Бендерски отлично поработал для меня. - person John McGehee; 31.07.2015

Если вы действительно хотите использовать несколько unitttest, вам понадобится несколько методов. Единственный способ добиться этого - создать своего рода код. Вы можете сделать это с помощью метаклассов или путем настройки класса после определения, в том числе (если вы используете Python 2.6) с помощью декоратора класса.

Вот решение, которое ищет специальные члены multitest и multitest_values ​​и использует их для создания методов тестирования на лету. Не элегантно, но делает примерно то, что вы хотите:

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.

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

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

person Andrew Dalke    schedule 05.11.2009

Думаю, вам нужны «параметризованные тесты».

Я не думаю, что модуль 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 простой декоратор, подобный этому, не сможет «воспроизвести» тест несколько раз, но я думаю, что это можно сделать, используя комбинацию декоратора и метакласса (метакласс должен соблюдать все методы 'test *' и реплицировать (под разными автоматически сгенерированными именами) те, к которым применен декоратор).

person anonymous    schedule 04.11.2009

Подход, более ориентированный на данные, может быть более ясным, чем тот, который используется в Эндрю Далке : //stackoverflow.com/questions/1676269/writing-a-re-usable-unittest-testcase-method#answer-1680087 "> ответ:

"""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.

person intuited    schedule 18.08.2010

Напишите единый тестовый метод, который выполняет все ваши тесты и фиксирует все результаты, напишите свои собственные диагностические сообщения в 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.

person Robert Rossney    schedule 05.11.2009

Возможно что-то вроде:

def test_many(self):
    for n in range(0,1000):
        self.assertEqual(self.somevalue, n)
person pojo    schedule 04.11.2009
comment
Это не то, что я ищу, потому что это прекратится, как только один из тестов не удастся. Я ищу решение, при котором сбой одного теста не останавливает выполнение других. - person astrofrog; 04.11.2009
comment
@Morgoth: Почему? Зачем проводить больше тестов, если вы знаете, что у вас сбой? - person S.Lott; 04.11.2009
comment
Потому что нечего сказать, другие тесты тоже не пройдут. Важно знать, все ли тесты терпят неудачу, или только один или два, поскольку это может помочь диагностировать проблему. Приятно с самого начала точно знать, сколько у вас сбоев, вам не нужно исправлять их одну за другой, пока они не прекратятся. - person astrofrog; 04.11.2009
comment
@Morgoth: Вы перешли от unittest к отладке или диагностике. Пакет unittest не предназначен для всего. Просто докажите, что все тесты проходят - не более того. Все ваши неудачи без написания нескольких методов могут оказаться за гранью конверта. - person S.Lott; 04.11.2009
comment
Но вот как работает unittest - он должен запускать все тесты, даже если некоторые из них не работают! Я просто пытаюсь сохранить исходное поведение unittest и в то же время сохранить код. Я не пытаюсь заставить unittest делать то, для чего он не предназначен. - person astrofrog; 04.11.2009
comment
Он запускает все методы тестирования; но вы не хотите писать все эти методы тестирования. Загадка. В самом деле, если все, что вы делаете, это пишете тестовые методы для принудительного запуска ряда тестов, я не уверен, что это уместно. Относительно сложно обойтись без обхода различных методов assert. Если вы это сделаете, зачем использовать unittest? - person S.Lott; 05.11.2009
comment
Если вы сочтете оправданным ожидание проведения всех тестов - они не только для того, чтобы увидеть, все ли тесты пройдены. А если нет? Скажите разработчикам, что все еще не так, попробуйте еще раз? Не нужно так защищаться о том, что, возможно, является ограничением указанного пакета unittest. - person peterchen; 19.08.2010