Тест блоку 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не викликали найперших ?

EDIT: Замість виконання 5 тестів вище, я хочу, щоб він виконував лише 4 тести, 2 із SubTest1 та ще 2 від SubTest2. Здається, що Python unittest запускає оригінальний BaseTest самостійно, і мені потрібен механізм, щоб не допустити цього.


Я бачу, що ніхто не згадував про це, але чи є у вас можливість змінити основну частину та запустити тестовий набір, у якому є всі підкласи BaseTest?
kon psych

Відповіді:


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, інакше вони взагалі не будуть викликатися.
Ian Clelland

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
Я серйозно не розумію цього (звідки береться магія?), Але, на мою думку, це найкраще рішення :) Починаючи з Яви, я ненавиджу множинне спадкування ...
Едуард Берт

4
@Edouardb unittest запускає лише класи рівня модулів, які успадковуються від TestCase. Але BaseTest не є рівнем модуля.
JoshB

Як дуже подібну альтернативу, ви можете визначити ABC всередині функції no-args, яка повертає ABC при
виклику

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 є членом модуля під час його визначення, тому він доступний для використання як базовий клас SubTests. Незадовго до того, як визначення буде завершене, del () видаляє його як член, тому фреймворк unittest не знайде його, коли він шукає підкласи TestCase в модулі.
mhsmith

3
це приголомшлива відповідь! Мені це подобається більше, ніж @MatthewMarshall, тому що в його рішенні ви отримаєте синтаксичні помилки від pylint, оскільки self.assert*методи не існують у стандартному об'єкті.
SimplyK knownAsG

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
Це акуратно. Чи є спосіб обійти необхідність скористатися пропуском? Для мене пропуски не бажані і використовуються для позначення проблеми в поточному плані тестування (або з кодом, або з тестом)?
Zach Young

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

@ZacharyYoung Я намагався виправити цю проблему, дивіться свою відповідь.
simonzack

не відразу зрозуміло, що по суті є схильним до помилок у спадкуванні з двох класів
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 () все ще запускається автоматично при тестуванні підкласів?
Стюарт

@Stewart Ні, це не було б. За замовчуванням налаштовано лише запуск методів, починаючи з "тестування".
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()

Це рішення не працює з власним тестовим відкриттям / тестовим запуском. (Я вважаю, що це вимагає використання альтернативного бігу для тесту, як ніс.)
medmunds

4

Інший варіант - не виконувати

unittest.main()

Замість цього ви можете використовувати

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

Таким чином, ви виконуєте лише тести в класі TestClass


Це найменш хакітичне рішення. Замість того, щоб змінювати те, що unittest.main()збирається у пакет за замовчуванням, ви формуєте явний набір і запускаєте його тести.
zgoda

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 (зазвичай) пропускає все, що не має в ньому тесту.

Швидкий і простий

  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 в жодному з SubTests.
Pepper Lebeck-Jobe

0

Отже, це якась стара тема, але я сьогодні зіткнувся з цією проблемою і подумав про свій власний злом. Він використовує декоратор, який робить значення функцій None, коли це доступ до базового класу. Не потрібно турбуватися про налаштування та setupclass, тому що якщо базовий клас не має тестів, вони не запускаються.

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" запускає всі тестові * методи в класі - саме так, як правило, використовують тести TestCase. Але працюючи в самому абстрактному базовому класі, ви можете просто перекрити таку поведінку методом, який нічого не робить.

Побічний ефект - кількість випробувань збільшиться на одиницю: "тест" runNoTestsInBaseClass вважається успішним тестом, коли він запускається на 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 Calling
SubTest2: testSub2

З документації :


Метод TestCase.setUp () викликається для підготовки тестового приладдя. Це викликається безпосередньо перед викликом методу тестування; будь-який виняток, піднятий цим методом, вважатиметься помилкою, а не тестом. Реалізація за замовчуванням нічого не робить.


Це спрацювало б. Що, якщо у мене є n testCommon, я повинен розміщувати їх усіх setUp?
Тіррі Лам

1
Так, слід встановити весь код, який не є фактичним тестовим випадком, під налаштуванням.
Брайан Р. Бонді

Але якщо підклас має більше одного test...методу, setUpвін виконується знову і знову і знову, один раз за такий метод; тож НЕ БЕЗДУМНО ставити там тести!
Алекс Мартеллі

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