Порядок виконання Python unittest.TestCase


80

Чи є в Python unittestспосіб встановити порядок запуску тестових випадків?

У моєму поточному TestCaseкласі деякі тестові випадки мають побічні ефекти, які встановлюють умови для нормальної роботи інших. Тепер я усвідомлюю, що правильний спосіб зробити це - використовувати setUp()всі налаштування реальних речей, але я хотів би реалізувати дизайн, при якому кожен наступний тест будує трохи більше стану, який може використовувати наступний. Я вважаю це набагато елегантнішим.

class MyTest(TestCase):
  def test_setup(self):
   #do something
  def test_thing(self)
   #do something that depends on test_setup()

В ідеалі я хотів би, щоб тести проводились у тому порядку, в якому вони з’являються в класі. Здається, вони працюють в алфавітному порядку.

Відповіді:


73

Не робіть їх незалежними тестами - якщо ви хочете монолітний тест, напишіть монолітний тест.

class Monolithic(TestCase):
  def step1(self):
      ...

  def step2(self):
      ...

  def _steps(self):
    for name in dir(self): # dir() result is implicitly sorted
      if name.startswith("step"):
        yield name, getattr(self, name) 

  def test_steps(self):
    for name, step in self._steps():
      try:
        step()
      except Exception as e:
        self.fail("{} failed ({}: {})".format(step, type(e), e))

Якщо пізніше тест почнеться невдалим, і ви хочете отримати інформацію про всі невдалі кроки, а не зупиняти тест на першому невдалому кроці, ви можете скористатися subtestsфункцією: https://docs.python.org/3/library/unittest.html# розрізнення-тестування-ітерації-використання-підтестів

(Функція підтесту доступна unittest2для версій до Python 3.4: https://pypi.python.org/pypi/unittest2 )


Я зовсім новачок у модульному тестуванні, і я відчуваю, що монолітний тест поганий. Це правда? Просто побудуйте мій набір тестів, і я дійсно залежать від монолітного тесту, використовуючи ваш код. Це ознака того, що я погано підходжу до модульного тестування? Дякую
swdev

8
Чисті одиничні тести дають перевагу в тому, що коли вони не вдаються, вони часто говорять вам , що саме не так. Ви також можете просто повторити тести, які не вдалося виправити. Такі монолітні тести не мають цих переваг: коли вони не вдаються, це налагоджувальна вправа, щоб з’ясувати, що пішло не так. З іншого боку, такі тести часто набагато простіші та швидші для написання, особливо при переобладнанні тестів на існуючу програму, яка не була побудована з урахуванням модульного тестування.
ncoghlan

5
@shakirthow Якщо порядок виконання має значення, вони більше не є модульними тестами - це кроки в сценарії тесту. Це все ще варто зробити, але це найкраще обробляти або як більший тест, як показано, або використовуючи систему поведінкового тестування вищого рівня, як pythonhosted.org/behave
ncoghlan

1
Зверніть увагу, що у вашому коді sorted()насправді немає необхідності, оскільки dir()повертає крокові методи за алфавітом, відсортовані за гарантією. Ось чому також unittestза замовчуванням обробляє тестові класи та методи тестування в алфавітному порядку (навіть коли sortTestMethodsUsingнемає) - що можна використати для практичності, наприклад, щоб спочатку запускати найновіші робочі тести для прискорення циклу редагування-теструн.
kxr 02.03.17

1
@ncoghlan Nick, просто хотів подякувати вам за ці коментарі щодо тестування - справді відкрив мені очі на проблему, яку я мав. Я також переслідував деякі інші ваші відповіді, які були однаково чудовими. На здоров’я!
Брендон Бертельсен,

41

Це хороша практика завжди писати монолітний тест для таких очікувань, однак, якщо ви такий дурний чувак, як я, тоді ви можете просто писати некрасиві на вигляд методи в алфавітному порядку, щоб вони були відсортовані від a до b, як згадується в документах python http : //docs.python.org/library/unittest.html

Зверніть увагу, що порядок запуску різних тестових випадків визначається шляхом сортування назв тестових функцій щодо вбудованого впорядкування для рядків

ПРИКЛАД:

  def test_a_first():
  print "1"
  def test_b_next(): 
  print "2" 
  def test_c_last(): 
  print "3"

5
ІМО такий підхід кращий, ніж додавання більше коду як обхідного шляху.
Raptor

Чому ви вважаєте, що це хороша практика писати монолітні тести? Ознайомтеся з більш досконалим способом, як Java TestNG робить це з тестовими групами та залежностями. У будь-якому випадку, я теж тупий хлопець, і коли я пишу свої тести в альфа-порядку, мені було корисно передавати стан через глобальні змінні, тому що тест-драйвер може створювати різні екземпляри для кожного тесту.
Джошуа Річардсон,

1
@Joshua Як і будь-яка інша річ, немає "єдиного рішення, щоб керувати ними всіма", рішення Monolithinc часто є гарною практикою проектування, яку розглядають деякі програмісти, замовлені тести або тести, керовані сценарієм, порушують одне з правил проектування для тестування, яке - це "один тест на очікування", але вам не потрібно цього дотримуватися. Я не великий шанувальник Java, і те, що фреймворк намагається щось зробити, не означає, що це хороша практика. І група тестових слів сама не робить для мене сенсу, але сміливо роби будь-який бруд.
варун

Існує тест, який, на мою думку, повинен виконуватися в останню чергу, а не посередині, тому почнемо його назву з "z".
кардамон

26

http://docs.python.org/library/unittest.html

Зверніть увагу, що порядок запуску різних тестових випадків визначається сортуванням назв тестових функцій щодо вбудованого впорядкування для рядків.

Тож просто переконайтесь, test_setupщо ім’я має найменше значення рядка.

Зауважте, що вам не слід покладатися на таку поведінку - різні функції тесту повинні бути незалежними від порядку виконання. Див. Відповідь ngcohlan вище, щоб знайти рішення, якщо вам явно потрібне замовлення.


6
Різний теструнер, інша поведінка. Ваша порада не буде корисною для написання стабільних тестів snd.
Андреас Юнг,

20

Старе запитання, але ще один спосіб, якого я не бачив у списку жодних пов’язаних питань: Використовуйте aTestSuite .

Інший спосіб виконати замовлення - це додати тести до unitest.TestSuite. Здається, це поважає порядок додавання тестів до набору за допомогою suite.addTest(...). Зробити це:

  • Створіть один або кілька підкласів TestCase,

    class FooTestCase(unittest.TestCase):
        def test_ten():
            print('Testing ten (10)...')
        def test_eleven():
            print('Testing eleven (11)...')
    
    class BarTestCase(unittest.TestCase):
        def test_twelve():
            print('Testing twelve (12)...')
        def test_nine():
            print('Testing nine (09)...')
    
  • Створіть викличне покоління тестового набору, додане у вашому бажаному порядку , адаптоване з документів та цього запитання :

    def suite():
        suite = unittest.TestSuite()
        suite.addTest(BarTestCase('test_nine'))
        suite.addTest(FooTestCase('test_ten'))
        suite.addTest(FooTestCase('test_eleven'))
        suite.addTest(BarTestCase('test_twelve'))
        return suite
    
  • Виконайте набір тестів, наприклад,

    if __name__ == '__main__':
        runner = unittest.TextTestRunner(failfast=True)
        runner.run(suite())
    

Для контексту, у мене була потреба в цьому, і мене не влаштовували інші варіанти. Я зупинився на вищезазначеному способі замовлення тестів. Я не бачив у цьому методі TestSuite жодного з декількох "питань замовлення модульних тестів" (наприклад, це питання та інші, включаючи порядок виконання , або зміну порядку , або порядок тестування ).


це добре, за винятком того, що він створює новий клас для кожного тесту. чи є спосіб зберегти дані з test_ten і використовувати їх у test_eleven?
thang

@thang, якщо ви робите речі, @classmethodвони можуть зберігатись у всіх інстанціях.
Nick Chapman

Роблячи це, ви знаєте, чи setUpClassвикликається? Або його потрібно запускати вручну?
Nick Chapman

@NickChapman, як це має сенс? @ classmethod майже робить його статичною функцією (з інформацією про клас як параметром)
thang

1
@thang @classmethod != @staticmethod!!! Будьте обережні, це абсолютно різні речі. @staticmethodдозволить вам викликати метод, не маючи екземпляра класу. @classmethodдає вам доступ до класу, а в самому класі ви можете зберігати інформацію. Наприклад, якщо ви робите cls.somevar = 10всередині методу класу, то всі екземпляри цього класу та всі інші методи класу побачать, що somevar = 10після запуску цієї функції. Самі класи - це об’єкти, на які ви можете прив’язати значення.
Нік Чепмен

4

У підсумку я отримав просте рішення, яке підійшло мені:

class SequentialTestLoader(unittest.TestLoader):
    def getTestCaseNames(self, testCaseClass):
        test_names = super().getTestCaseNames(testCaseClass)
        testcase_methods = list(testCaseClass.__dict__.keys())
        test_names.sort(key=testcase_methods.index)
        return test_names

І потім

unittest.main(testLoader=utils.SequentialTestLoader())

1

Тести, які насправді залежать один від одного, повинні бути чітко об'єднані в один тест.

Тести, які вимагають різних рівнів налаштування, можуть також мати відповідні setUp()достатньо запущені налаштування - різними способами, які можна зрозуміти.

В іншому випадку unittestобробляє тестові класи та методи тестування всередині тестових класів в алфавітному порядку за замовчуванням (навіть коли loader.sortTestMethodsUsingнемає). dir()використовується всередині, що сортується за гарантією.

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


0

Відповідь @ ncoghlan була саме тим, що я шукав, коли прийшов до цієї теми. У підсумку я змінив його, щоб дозволити запускати кожен кроковий тест, навіть якщо попередній крок вже спричинив помилку; це допомагає мені (а може і вам!) виявити та спланувати розповсюдження помилок у багатопотоковому програмному забезпеченні, орієнтованому на базу даних.

class Monolithic(TestCase):
  def step1_testName1(self):
      ...

  def step2_testName2(self):
      ...

  def steps(self):
      '''
      Generates the step methods from their parent object
      '''
      for name in sorted(dir(self)):
          if name.startswith('step'):
              yield name, getattr(self, name)

  def test_steps(self):
      '''
      Run the individual steps associated with this test
      '''
      # Create a flag that determines whether to raise an error at
      # the end of the test
      failed = False

      # An empty string that the will accumulate error messages for 
      # each failing step
      fail_message = ''
      for name, step in self.steps():
          try:
              step()
          except Exception as e:
              # A step has failed, the test should continue through
              # the remaining steps, but eventually fail
              failed = True

              # get the name of the method -- so the fail message is
              # nicer to read :)
              name = name.split('_')[1]
              # append this step's exception to the fail message
              fail_message += "\n\nFAIL: {}\n {} failed ({}: {})".format(name,
                                                                       step,
                                                                       type(e),
                                                                       e)

      # check if any of the steps failed
      if failed is True:
          # fail the test with the accumulated exception message
          self.fail(fail_message)

0

Простий та гнучкий спосіб - призначити функцію порівняння unittest.TestLoader.sortTestMethodsUsing:

Функція, яка використовується для порівняння назв методів при їх сортуванні getTestCaseNames()та всіх loadTestsFrom*()методів.

Мінімальне використання:

import unittest

class Test(unittest.TestCase):
    def test_foo(self):
        """ test foo """
        self.assertEqual(1, 1)

    def test_bar(self):
        """ test bar """
        self.assertEqual(1, 1)

if __name__ == "__main__":
    test_order = ["test_foo", "test_bar"] # could be sys.argv
    loader = unittest.TestLoader()
    loader.sortTestMethodsUsing = lambda x, y: test_order.index(x) - test_order.index(y)
    unittest.main(testLoader=loader, verbosity=2)

Вихід:

test_foo (__main__.Test)
test foo ... ok
test_bar (__main__.Test)
test bar ... ok

Ось доказ концепції запуску тестів у порядку вихідного коду замість лексичного порядку за замовчуванням (вихід такий, як вище).

import inspect
import unittest

class Test(unittest.TestCase):
    def test_foo(self):
        """ test foo """
        self.assertEqual(1, 1)

    def test_bar(self):
        """ test bar """
        self.assertEqual(1, 1)

if __name__ == "__main__":
    test_src = inspect.getsource(Test)
    unittest.TestLoader.sortTestMethodsUsing = lambda _, x, y: (
        test_src.index(f"def {x}") - test_src.index(f"def {y}")
    )
    unittest.main(verbosity=2)

Я використовував Python 3.8.0 у цій публікації.


0

Один із підходів може полягати в тому, щоб дозволити, щоб ці підтести не розглядалися як тести за допомогою unittestмодуля, додаючи їх _перед собою, а потім будуючи тестовий кейс, який ґрунтується на правильному порядку цих субоперацій.

Це краще, ніж покладатися на порядок сортування unittestмодуля, оскільки це може змінитися завтра, а також досягнення топологічного сортування за порядком буде не дуже простим.

Приклад такого підходу, взятий звідси (Застереження: мій власний модуль) , поданий нижче.

Тут тестовий приклад запускає незалежні тести, такі як перевірка на параметр таблиці не встановлений ( test_table_not_set) або тест на первинний ключ ( test_primary_key) все ще паралельно, але CRUD-тест має сенс лише у тому випадку, якщо виконаний у правильному порядку та стані, встановленому попередніми операціями. Отже, ці тести були зроблені досить окремо, unitале не тестом. test_CRUDПотім інший тест ( ) створює правильний порядок цих операцій і тестує їх.

import os
import sqlite3
import unittest

from sql30 import db

DB_NAME = 'review.db'


class Reviews(db.Model):
    TABLE = 'reviews'
    PKEY = 'rid'
    DB_SCHEMA = {
        'db_name': DB_NAME,
        'tables': [
            {
                'name': TABLE,
                'fields': {
                    'rid': 'uuid',
                    'header': 'text',
                    'rating': 'int',
                    'desc': 'text'
                    },
                'primary_key': PKEY
            }]
        }
    VALIDATE_BEFORE_WRITE = True

class ReviewTest(unittest.TestCase):

    def setUp(self):
        if os.path.exists(DB_NAME):
            os.remove(DB_NAME)

    def test_table_not_set(self):
        """
        Tests for raise of assertion when table is not set.
        """
        db = Reviews()
        try:
            db.read()
        except Exception as err:
            self.assertIn('No table set for operation', str(err))

    def test_primary_key(self):
        """
        Ensures , primary key is honored.
        """
        db = Reviews()
        db.table = 'reviews'
        db.write(rid=10, rating=5)
        try:
            db.write(rid=10, rating=4)
        except sqlite3.IntegrityError as err:
            self.assertIn('UNIQUE constraint failed', str(err))

    def _test_CREATE(self):
        db = Reviews()
        db.table = 'reviews'
        # backward compatibility for 'write' API
        db.write(tbl='reviews', rid=1, header='good thing', rating=5)

        # New API with 'create'
        db.create(tbl='reviews', rid=2, header='good thing', rating=5)

        # backward compatibility for 'write' API, without tbl,
        # explicitly passed
        db.write(tbl='reviews', rid=3, header='good thing', rating=5)

        # New API with 'create', without table name explicitly passed.
        db.create(tbl='reviews', rid=4, header='good thing', rating=5)

        db.commit()   # save the work.

    def _test_READ(self):
        db = Reviews()
        db.table = 'reviews'

        rec1 = db.read(tbl='reviews', rid=1, header='good thing', rating=5)
        rec2 = db.read(rid=1, header='good thing')
        rec3 = db.read(rid=1)

        self.assertEqual(rec1, rec2)
        self.assertEqual(rec2, rec3)

        recs = db.read()  # read all
        self.assertEqual(len(recs), 4)

    def _test_UPDATE(self):
        db = Reviews()
        db.table = 'reviews'

        where = {'rid': 2}
        db.update(condition=where, header='average item', rating=2)
        db.commit()

        rec = db.read(rid=2)[0]
        self.assertIn('average item', rec)

    def _test_DELETE(self):
        db = Reviews()
        db.table = 'reviews'

        db.delete(rid=2)
        db.commit()
        self.assertFalse(db.read(rid=2))

    def test_CRUD(self):
        self._test_CREATE()
        self._test_READ()
        self._test_UPDATE()
        self._test_DELETE()

    def tearDown(self):
        os.remove(DB_NAME)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.