абстрактный тестовый пример с использованием Python Unittest - PullRequest
42 голосов
/ 31 декабря 2010

Можно ли создать абстрактный TestCase, который будет иметь несколько методов test_ *, но этот TestCase не будет вызываться, и эти методы будут использоваться только в подклассах?Я думаю, что у меня будет один абстрактный TestCase в моем наборе тестов, и он будет разделен на подклассы для нескольких различных реализаций одного интерфейса.Вот почему все методы тестирования - это некоторые, только один, внутренние изменения метода.Как я могу сделать это элегантно?

Ответы [ 8 ]

62 голосов
/ 31 декабря 2010

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

Но для достижения того, что выхочу, если вы наследуете от unittest.TestCase, всякий раз, когда вы вызываете unittest.main (), ваш «абстрактный» класс будет выполняться - я думаю, что это ситуация, которую вы хотите избежать.

Просто сделайте это: Создайте свой"абстрактный" класс, наследующий от "объекта", а не от TestCase.А для реальных «конкретных» реализаций просто используйте множественное наследование: наследуйте как от unittest.TestCase, так и от вашего абстрактного класса.

import unittest

class Abstract(object):
    def test_a(self):
        print "Running for class", self.__class__

class Test(Abstract, unittest.TestCase):
    pass

unittest.main()

update : обратный порядок наследования - Abstract сначала, чтобы его определения не были переопределены TestCase значениями по умолчанию, что также указано в комментариях ниже.

11 голосов
/ 04 августа 2013

Множественное наследование не является хорошим вариантом, главным образом по двум следующим причинам:

  1. Ни один из методов в TestCase не использует super(), поэтому вам нужно сначала указать свой класс, чтобы такие методы, как setUp() и tearDown() работали.
  2. pylint предупредит, что базовый класс использует self.assertEquals() и т. Д., Которые не определены в self в этой точке.

Вот клудж, который я придумал: превратите run() в неработоспособность только для базового класса.

class TestBase( unittest.TestCase ):

  def __init__( self, *args, **kwargs ):
    super( TestBase, self ).__init__( *args, **kwargs )
    self.helper = None
    # Kludge alert: We want this class to carry test cases without being run
    # by the unit test framework, so the `run' method is overridden to do
    # nothing.  But in order for sub-classes to be able to do something when
    # run is invoked, the constructor will rebind `run' from TestCase.
    if self.__class__ != TestBase:
      # Rebind `run' from the parent class.
      self.run = unittest.TestCase.run.__get__( self, self.__class__ )                          
    else:
      self.run = lambda self, *args, **kwargs: None

  def newHelper( self ):
    raise NotImplementedError()

  def setUp( self ):
    print "shared for all subclasses"
    self.helper = self.newHelper()

  def testFoo( self ):
    print "shared for all subclasses"
    # test something with self.helper

class Test1( TestBase ):
  def newHelper( self ):
    return HelperObject1()

class Test2( TestBase ):
  def newHelper( self ):
    return HelperObject2()
6 голосов
/ 11 апреля 2017

Существует очень простой способ, который все до сих пор упускали.И, в отличие от некоторых ответов, он работает с всеми тестовыми драйверами, а не с ошибкой в ​​минуту, когда вы переключаетесь между ними.

Просто используйте наследование как обычно, затем добавьте:

del AbstractTestCase

в конце модуля.

6 голосов
/ 05 сентября 2014

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

from django.test import TestCase


class _AbstractTestCase(TestCase):

    """
    Abstract test case - should not be instantiated by the test runner.
    """

    def test_1(self):
        raise NotImplementedError()

    def test_2(self):
        raise NotImplementedError()


class TestCase1(_AbstractTestCase):

    """
    This test case will pass and fail.
    """

    def test_1(self):
        self.assertEqual(1 + 1, 2)


class TestCase2(_AbstractTestCase):

    """
    This test case will pass successfully.
    """

    def test_1(self):
        self.assertEqual(2 + 2, 4)

    def test_2(self):
        self.assertEqual(12 * 12, 144)
4 голосов
/ 31 декабря 2010

Если вы следуете соглашению о явном перечислении всех тестовых классов в run_unittest (см., Например, набор тестов Python для многих применений этого соглашения), то будет просто , а не перечислить определенный класс.

Если вы хотите продолжить использовать unittest.main и если вы можете разрешить использование unittest2 (например, из Python 2.7), вы можете использовать его протокол load_tests , чтобы указать, какие классы содержат тестовые случаи).В более ранних версиях вам придется создавать подкласс TestLoader и переопределять loadTestsFromModule .

2 голосов
/ 16 июня 2015

Библиотека юнит-тестов Python имеет протокол load_tests , который может использоваться для достижения именно того, что вы хотите:

# Add this function to module with AbstractTestCase class
def load_tests(loader, tests, _):
    result = []
    for test_case in tests:
        if type(test_case._tests[0]) is AbstractTestCase:
            continue
        result.append(test_case)
    return loader.suiteClass(result)
1 голос
/ 04 мая 2018

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

Это позволяет избежать проблем с обнаружением бегуна тестов, и вы все равно можете импортировать абстрактный тест из другого класса.модуль.

import unittest

class AbstractTests(object):
    class AbstractTest(unittest.TestCase)
        def test_a(self):
            print "Running for class", self.__class__

class Test(AbstractTests.AbstractTest):
    pass
0 голосов
/ 19 октября 2018

Другая причина желания делать то, что делает OP, заключается в создании высокопараметризованного базового класса, который реализует большую часть набора базовых тестов, которые необходимо воспроизвести в нескольких средах / сценариях. То, что я описываю, это по сути создание параметризованного прибора, в виде pytest, с использованием unittest.

Предполагая, что вы (как и я) решили убежать как можно быстрее от любых решений, основанных на множественном наследовании, может возникнуть следующая проблема с использованием load_tests () для фильтрации вашего базового класса из загруженного набора:

В стандартном TestLoader load_tests называется после автоматическая загрузка из класса завершена. Так как: * эта автозагрузка из класса будет пытаться создать экземпляры из вашего базового класса со стандартной подписью init (self, name) и * вы можете захотеть, чтобы у этого базового класса была другая подпись ctor, или * вы можете пропустить создание-затем-удаление экземпляров базового класса по какой-то другой причине

.. вы можете полностью запретить автоматическую загрузку тестовых экземпляров из базовых классов.

РЕДАКТИРОВАТЬ: Решение Вадима в этом другом потоке является более элегантным, лаконичным и независимым способом сделать это. Я реализовал «трюк с вложенными классами» и подтвердил, что он прекрасно работает с целью помешать TestLoader «найти» ваши базы TestCase.

Первоначально я сделал это, изменив TestLoader.loadTestsFromModule, чтобы просто пропустить любые классы TestCase, которые служат базовыми классами для любых других классов TestCase в модуле:

for name in dir(module):
    obj = getattr(module, name)
    # skip TestCase classes:
    # 1. without any test methods defined
    # 2. that are base classes
    #    (we don't allow instantiating TestCase base classes, which allows test designers
    #     to implement actual test methods in highly-parametrized base classes.)
    if isinstance(obj, type) and issubclass(obj, unittest.TestCase) and \
            self.getTestCaseNames(obj) and not isbase(obj, module):
        loaded_suite = self.loadTestsFromTestCase(obj)
        # ignore empty suites
        if loaded_suite.countTestCases():
            tests.append(loaded_suite)

где:

def isbase(cls, module):
    '''Returns True if cls is base class to any classes in module, else False.'''
    for name in dir(module):
        obj = getattr(module, name)
        if obj is not cls and isinstance(obj, type) and issubclass(obj, cls):
            return True
    return False

Параметризация, о которой я говорил выше, реализуется с помощью того, чтобы каждый дочерний класс определял детали своего прибора (параметры) и передавал их в базовый класс TestCase ctor, чтобы все его общие методы impl ("fixturey" setUp * / tearDown * / cleanup * и сами методы тестирования) имеют всю информацию, которая определяет теперь очень специфическое приспособление, с которым должен работать этот дочерний класс TestCase.

Для меня это было временное решение для быстрой реализации некоторых параметризованных приборов в unittest, так как я планирую перенести тесты моей команды на pytest как можно скорее.

...