Как я могу кратко реализовать несколько аналогичных модульных тестов в среде юнит-тестов Python? - PullRequest
14 голосов
/ 07 декабря 2008

Я реализую модульные тесты для семейства функций, которые имеют несколько инвариантов. Например, вызов функции с двумя матрицами создает матрицу известной формы.

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

Один из способов сделать это - перебрать список этих функций:

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, но тестовый сценарий не распознал их.

Как я могу переписать это, чтобы тривиально было расширить список тестов, и при этом обеспечить выполнение каждого теста?

Ответы [ 9 ]

11 голосов
/ 07 декабря 2008

Вот мой любимый подход к «семейству связанных тестов». Мне нравятся явные подклассы 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()

5 голосов
/ 29 декабря 2009

Вам не нужно использовать метаклассы здесь. Простой цикл подходит просто отлично. Взгляните на пример ниже:

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 метода тестирования, которые поддерживаются как носом, так и модулем тестирования.

5 голосов
/ 17 декабря 2008

Если вы уже используете нос (и некоторые ваши комментарии предполагают, что вы это делаете), почему бы вам просто не использовать Генераторы тестов , которые являются наиболее простым способом реализации параметрических тестов, которые у меня есть? попадаются:

Например:

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 голосов
/ 07 декабря 2008

Вы можете использовать метакласс для динамической вставки тестов. Это прекрасно работает для меня:

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()
3 голосов
/ 17 мая 2015

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

1 голос
/ 24 августа 2009

В приведенном выше коде метакласса есть проблема с носом, потому что объект 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
1 голос
/ 07 декабря 2008

Метаклассы - это один из вариантов. Другой вариант - использовать 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()

Не проверял. Но это должно работать нормально.

0 голосов
/ 17 февраля 2011

Я прочитал приведенный выше пример метакласса, и он мне понравился, но в нем отсутствовали две вещи:

  1. Как управлять им со структурой данных?
  2. Как убедиться, что тестовая функция написана правильно?

Я написал этот более полный пример, который управляется данными и в котором тестовая функция сама тестируется модулем.

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)
0 голосов
/ 17 декабря 2008

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...