Как кратко реализовать несколько аналогичных модульных тестов в рамочной системе 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.

Ответ 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

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

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

Как только этот тест пройдет, остальные тесты будут запущены.

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