Модульный тест Python с базовым и подклассом


149

В настоящее время у меня есть несколько модульных тестов, которые используют общий набор тестов. Вот пример:

import unittest

class BaseTest(unittest.TestCase):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

Результат вышеупомянутого:

Calling BaseTest:testCommon
.Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

Есть ли способ переписать вышесказанное, чтобы testCommonне назывался самый первый ?

РЕДАКТИРОВАТЬ: вместо запуска 5 тестов выше, я хочу, чтобы он запускал только 4 теста, 2 из SubTest1 и еще 2 из SubTest2. Кажется, что Python unittest запускает оригинальный BaseTest сам по себе, и мне нужен механизм, чтобы предотвратить это.


Я вижу, никто не упомянул об этом, но есть ли у вас возможность изменить основную часть и запустить тестовый набор, который имеет все подклассы BaseTest?
Кон Псих

Ответы:


154

Используйте множественное наследование, поэтому ваш класс с общими тестами сам по себе не наследуется от TestCase.

import unittest

class CommonTests(object):
    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(unittest.TestCase, CommonTests):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(unittest.TestCase, CommonTests):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

1
Это самое элегантное решение на сегодняшний день.
Тьерри Лэм

27
Этот метод работает только для методов setUp и tearDown, если вы измените порядок базовых классов. Поскольку методы определены в unittest.TestCase, и они не вызывают super (), то любые методы setUp и tearDown в CommonTests должны быть первыми в MRO, или они вообще не будут вызываться.
Ян Клелланд

32
Просто чтобы прояснить замечание Яна Клелланда, чтобы оно было более понятным для таких людей, как я: если вы добавляете setUpи tearDownметоды в CommonTestsкласс и хотите, чтобы они вызывались для каждого теста в производных классах, вы должны изменить порядок базовых классов, так что это будет: class SubTest1(CommonTests, unittest.TestCase).
Денис Голомазов

6
Я не очень фанат такого подхода. Это устанавливает контракт в коде, который классы должны наследовать от обоих unittest.TestCase и CommonTests . Я думаю, что setUpClassметод ниже является лучшим и менее подвержен человеческим ошибкам. Либо это, либо упаковка класса BaseTest в контейнерный класс, который немного более хакерский, но позволяет избежать пропуска сообщения в распечатке тестового прогона.
Дэвид Сандерс

10
Проблема с этим состоит в том, что pylint подходит, потому что CommonTestsвызывает методы, которые не существуют в этом классе.
MadScientist

146

Не используйте множественное наследование, это укусит вас позже .

Вместо этого вы можете просто переместить ваш базовый класс в отдельный модуль или обернуть его пустым классом:

class BaseTestCases:

    class BaseTest(unittest.TestCase):

        def testCommon(self):
            print('Calling BaseTest:testCommon')
            value = 5
            self.assertEqual(value, 5)


class SubTest1(BaseTestCases.BaseTest):

    def testSub1(self):
        print('Calling SubTest1:testSub1')
        sub = 3
        self.assertEqual(sub, 3)


class SubTest2(BaseTestCases.BaseTest):

    def testSub2(self):
        print('Calling SubTest2:testSub2')
        sub = 4
        self.assertEqual(sub, 4)

if __name__ == '__main__':
    unittest.main()

Выход:

Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

6
Это моя любимая. Это наименее хакерское средство и не мешает переопределению методов, не изменяет MRO и позволяет мне определять setUp, setUpClass и т. Д. В базовом классе.
Ханнес

6
Я серьезно не понимаю (откуда приходит магия?), Но, на мой взгляд, это лучшее решение :) Исходя из Java, я ненавижу множественное наследование ...
Эдуард Берт,

4
@Edouardb unittest запускает только классы уровня модуля, которые наследуются от TestCase. Но BaseTest не является модульным уровнем.
JoshB

В качестве очень похожей альтернативы вы можете определить ABC внутри функции без аргументов, которая возвращает ABC при вызове
Anakhand

34

Вы можете решить эту проблему с помощью одной команды:

del(BaseTest)

Таким образом, код будет выглядеть так:

import unittest

class BaseTest(unittest.TestCase):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

del(BaseTest)

if __name__ == '__main__':
    unittest.main()

3
BaseTest является членом модуля во время его определения, поэтому он доступен для использования в качестве базового класса подэлементов. Непосредственно перед завершением определения del () удаляет его как член, поэтому инфраструктура unittest не найдет его при поиске подклассов TestCase в модуле.
mhsmith

3
это потрясающий ответ! Мне это нравится больше, чем @MatthewMarshall, потому что в его решении вы получите синтаксические ошибки от pylint, потому что self.assert*методы не существуют в стандартном объекте.
SimplyKnownAsG

1
Не работает, если на BaseTest есть ссылка где-либо еще в базовом классе или его подклассах, например, при вызове super () в переопределениях методов: super( BaseTest, cls ).setUpClass( )
Hannes

1
@Hannes По крайней мере в Python 3, BaseTestможно ссылаться через super(self.__class__, self)или просто super()в подклассах, хотя, очевидно, нет, если вы должны были наследовать конструкторы . Возможно, существует и такая «анонимная» альтернатива, когда базовый класс должен ссылаться на себя (не то чтобы я имел представление, когда класс должен ссылаться на себя).
Стейн

29

Ответ Мэтью Маршалла великолепен, но он требует, чтобы вы унаследовали от двух классов в каждом из ваших тестовых случаев, что подвержено ошибкам. Вместо этого я использую это (python> = 2.7):

class BaseTest(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        if cls is BaseTest:
            raise unittest.SkipTest("Skip BaseTest tests, it's a base class")
        super(BaseTest, cls).setUpClass()

3
Это аккуратно. Есть ли способ обойти необходимость пропустить? Для меня пропуски нежелательны и используются для обозначения проблемы в текущем плане тестирования (либо с кодом, либо с тестом)?
Зак Янг

@ ZacharyYoung Я не знаю, может быть, другие ответы могут помочь.
Деннис Голомазов

@ZacharyYoung Я пытался решить эту проблему, см. Мой ответ.
Симонзак

не сразу понятно, что по своей природе подвержено ошибкам при наследовании от двух классов
jwg

@jwg смотрите комментарии к принятому ответу :) Вам необходимо унаследовать каждый из ваших тестовых классов от двух базовых классов; вам нужно сохранить правильный их порядок; Если вы хотите добавить еще один базовый тестовый класс, вам также нужно наследовать его. В миксинах нет ничего плохого, но в этом случае их можно заменить простым пропуском.
Денис Голомазов

7

Чего ты пытаешься достичь? Если у вас есть общий тестовый код (утверждения, тесты шаблонов и т. Д.), Поместите их в методы, для которых нет префикса, testпоэтому unittestне будут загружать их.

import unittest

class CommonTests(unittest.TestCase):
      def common_assertion(self, foo, bar, baz):
          # whatever common code
          self.assertEqual(foo(bar), baz)

class BaseTest(CommonTests):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(CommonTests):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)

class SubTest2(CommonTests):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

1
По вашему предложению, будет ли common_assertion () по-прежнему запускаться автоматически при тестировании подклассов?
Стюарт

@ Стюарт Нет, не будет. Настройка по умолчанию - запускать только методы, начинающиеся с «test».
CS

6

Ответ Мэтью - тот, который мне нужно было использовать, так как я все еще на 2.5. Но начиная с версии 2.7 вы можете использовать декоратор @ unittest.skip () для любых методов тестирования, которые хотите пропустить.

http://docs.python.org/library/unittest.html#skipping-tests-and-expected-failures

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

def skipBaseTest(obj):
    if type(obj) is BaseTest:
        return unittest.skip("BaseTest tests skipped")
    return lambda func: func

6

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

По сравнению с методом mixin, ide как PyCharm не будет жаловаться на то, что в базовом классе отсутствуют методы модульного тестирования.

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

class BaseTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls._test_methods = []
        if cls is BaseTest:
            for name in dir(cls):
                if name.startswith('test') and callable(getattr(cls, name)):
                    cls._test_methods.append((name, getattr(cls, name)))
                    setattr(cls, name, lambda self: None)

    @classmethod
    def tearDownClass(cls):
        if cls is BaseTest:
            for name, method in cls._test_methods:
                setattr(cls, name, method)
            cls._test_methods = []

5

Вы можете добавить __test_ = Falseкласс BaseTest, но если вы добавите его, помните, что вы должны добавить __test__ = Trueв производные классы, чтобы иметь возможность запускать тесты.

import unittest

class BaseTest(unittest.TestCase):
    __test__ = False

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):
    __test__ = True

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):
    __test__ = True

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()

Это решение не работает с собственным средством обнаружения / запуска тестов unittest. (Я полагаю, что это требует использования альтернативного тест-бегуна, например, носа.)
medmunds

4

Другой вариант не выполнить

unittest.main()

Вместо этого вы можете использовать

suite = unittest.TestLoader().loadTestsFromTestCase(TestClass)
unittest.TextTestRunner(verbosity=2).run(suite)

Таким образом, вы выполняете только тесты в классе TestClass


Это наименее хакерское решение. Вместо того, чтобы модифицировать то, что unittest.main()собирается в набор по умолчанию, вы формируете явный набор и запускаете его тесты.
Згода

1

Я сделал примерно то же самое, что @Vladim P. ( https://stackoverflow.com/a/25695512/2451329 ), но немного изменил:

import unittest2


from some_module import func1, func2


def make_base_class(func):

    class Base(unittest2.TestCase):

        def test_common1(self):
            print("in test_common1")
            self.assertTrue(func())

        def test_common2(self):
            print("in test_common1")
            self.assertFalse(func(42))

    return Base



class A(make_base_class(func1)):
    pass


class B(make_base_class(func2)):

    def test_func2_with_no_arg_return_bar(self):
        self.assertEqual("bar", func2())

и там мы идем.


1

Начиная с Python 3.2, вы можете добавить функцию test_loader в модуль, чтобы контролировать, какие тесты (если таковые имеются) обнаруживаются механизмом обнаружения тестов.

Например, следующее будет загружать только оригинальные плакаты SubTest1и SubTest2тестовые случаи, игнорируя Base:

def load_tests(loader, standard_tests, pattern):
    suite = TestSuite()
    suite.addTests([SubTest1, SubTest2])
    return suite

Должна быть возможность перебора standard_tests( TestSuiteсодержащая тесты, найденный загрузчик по умолчанию) и копирование всего, кроме Baseкак suiteвместо этого, но вложенная природа TestSuite.__iter__делает это намного сложнее.


0

Просто переименуйте метод testCommon во что-то другое. Unittest (обычно) пропускает все, что не имеет «test».

Быстро и просто

  import unittest

  class BaseTest(unittest.TestCase):

   def methodCommon(self):
       print 'Calling BaseTest:testCommon'
       value = 5
       self.assertEquals(value, 5)

  class SubTest1(BaseTest):

      def testSub1(self):
          print 'Calling SubTest1:testSub1'
          sub = 3
          self.assertEquals(sub, 3)


  class SubTest2(BaseTest):

      def testSub2(self):
          print 'Calling SubTest2:testSub2'
          sub = 4
          self.assertEquals(sub, 4)

  if __name__ == '__main__':
      unittest.main()`

2
Это может привести к тому, что тест methodCommon не будет выполнен ни в одном из подстестов.
Пеппер Лебек-Джобе

0

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

import types
import unittest


class FunctionValueOverride(object):
    def __init__(self, cls, default, override=None):
        self.cls = cls
        self.default = default
        self.override = override

    def __get__(self, obj, klass):
        if klass == self.cls:
            return self.override
        else:
            if obj:
                return types.MethodType(self.default, obj)
            else:
                return self.default


def fixture(cls):
    for t in vars(cls):
        if not callable(getattr(cls, t)) or t[:4] != "test":
            continue
        setattr(cls, t, FunctionValueOverride(cls, getattr(cls, t)))
    return cls


@fixture
class BaseTest(unittest.TestCase):
    def testCommon(self):
        print('Calling BaseTest:testCommon')
        value = 5
        self.assertEqual(value, 5)


class SubTest1(BaseTest):
    def testSub1(self):
        print('Calling SubTest1:testSub1')
        sub = 3
        self.assertEqual(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print('Calling SubTest2:testSub2')
        sub = 4
        self.assertEqual(sub, 4)

if __name__ == '__main__':
    unittest.main()

0

Вот решение, которое использует только документированные функции unittest и позволяет избежать «пропуска» статуса в результатах вашего теста:

class BaseTest(unittest.TestCase):

    def __init__(self, methodName='runTest'):
        if self.__class__ is BaseTest:
            # don't run these tests in the abstract base implementation
            methodName = 'runNoTestsInBaseClass'
        super().__init__(methodName)

    def runNoTestsInBaseClass(self):
        pass

    def testCommon(self):
        # everything else as in the original question

Как это работает: согласно unittest.TestCaseдокументации , «Каждый экземпляр TestCase будет запускать один базовый метод: метод с именем methodName». По умолчанию «runTests» запускает все методы test * в классе - именно так обычно работают экземпляры TestCase. Но при работе в самом абстрактном базовом классе вы можете просто переопределить это поведение с помощью метода, который ничего не делает.

Побочным эффектом является то, что число ваших тестов увеличится на единицу: runNoTestsInBaseClass «test» будет считаться успешным тестом при запуске на BaseClass.

(Это также работает в Python 2.7, если вы все еще на этом. Просто перейдите super()на super(BaseTest, self).)


-2

Измените имя метода BaseTest на setUp:

class BaseTest(unittest.TestCase):
    def setUp(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)


class SubTest1(BaseTest):
    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):
    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

Вывод:

Провел 2 теста за 0.000 с

Вызов BaseTest: testCommon Вызов
SubTest1: testSub1 Вызов
BaseTest: testCommon Вызов
SubTest2: testSub2

Из документации :

TestCase.setUp ()
Метод, вызываемый для подготовки тестового устройства. Это вызывается непосредственно перед вызовом тестового метода; любое исключение, вызванное этим методом, будет считаться ошибкой, а не провалом теста. Реализация по умолчанию ничего не делает.


Это будет работать, что если у меня есть n testCommon, я должен поместить их всех под setUp?
Тьерри Лэм

1
Да, вы должны поместить весь код, который не является реальным тестовым примером, в setUp.
Брайан Р. Бонди

Но если у подкласса есть больше чем один test...метод, он setUpвыполняется снова и снова, один раз для такого метода; так что не стоит ставить там тесты!
Алекс Мартелли

Не совсем уверен, что ОП хотел с точки зрения, когда выполняется в более сложном сценарии.
Брайан Р. Бонди
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.