Порядок виконання 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.