繁体   English   中英

Python unittest.TestCase 执行顺序

[英]Python unittest.TestCase execution order

Python unittest中有没有办法设置测试用例的运行顺序?

在我当前的TestCase class 中,一些测试用例具有副作用,为其他测试用例设置了正常运行的条件。 现在我意识到做到这一点的正确方法是使用setUp()来完成所有与设置相关的事情,但我想实现一个设计,其中每个连续的测试构建稍微多一点的 state 以便下一个可以使用。 我觉得这更优雅。

class MyTest(TestCase):

  def test_setup(self):
    # Do something

  def test_thing(self):
    # Do something that depends on test_setup()

理想情况下,我希望测试按照它们在 class 中出现的顺序运行。 它们似乎按字母顺序运行。

不要让它们成为独立的测试-如果您要进行整体测试,请编写整体测试。

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 #distinguishing-test-iterations-using-subtests

(子测试功能可通过unittest2用于Python 3.4之前的版本: https : //pypi.python.org/pypi/unittest2

始终为这样的期望编写整体测试是一个好习惯,但是,如果像我这样愚蠢的家伙,那么您可以简单地按字母顺序编写难看的方法,以便按照python docs http中提到的从a到b进行排序://docs.python.org/library/unittest.html

注意,各种测试用例的运行顺序是通过根据字符串的内置顺序对测试函数名称进行排序来确定的

例:

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

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

注意,各种测试用例的运行顺序是通过根据字符串的内置顺序对测试函数名称进行排序来确定的。

因此,只需确保test_setup的名称具有最小的字符串值即可。

请注意,您不应该依赖此行为-假定不同的测试功能与执行顺序无关。 如果您明确需要订单,请参阅上面的ngcohlan的答案以获取解决方案。

旧问题,但是在任何相关问题中都未列出的另一种方法: 使用TestSuite

完成订购的另一种方法是将测试添加到unitest.TestSuite 这似乎符合使用suite.addTest(...)将测试添加到套件中的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方法列出了几个“单元测试排序问题”中的任何一个(例如,这个问题以及其他一些问题,包括执行顺序更改顺序测试顺序 )。

一种简单灵活的方法是将比较器 function 分配给unittest.TestLoader.sortTestMethodsUsing

Function 用于在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)

Output:

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。

我最终得到了一个对我有用的简单解决方案:

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())

真正相互依赖的测试应明确地链接到一个测试中。

需要不同级别的安装程序的测试,也可以使它们相应的setUp()运行足够的安装程序-各种可行的方式。

否则, unittest在默认情况下(即使把手按字母顺序排列测试类中的测试类和测试方法loader.sortTestMethodsUsing是无)。 dir()在内部使用,按保证排序。

可以利用后一种行为的实用性 -例如,首先运行最新工作测试以加快edit-testrun-cycle。 但是,不应将这种行为用于建立真正的依赖关系 考虑到可以通过命令行选项等单独运行测试。

一种方法是让这些子测试不被unittest模块视为测试,方法是在它们前面附加_然后构建一个测试用例,该测试用例建立在这些子操作执行的正确顺序上。

这比依赖unittest模块的排序顺序要好,因为明天可能会改变,并且在顺序上实现拓扑排序也不是很简单。

取自此处(免责声明:我自己的模块)的这种方法的示例如下。

在这里,测试用例运行独立的测试,例如检查未设置的表参数( test_table_not_set )或测试主键( test_primary_key )仍然并行,但CRUD测试只有在以正确的顺序完成并且 state 由先前的操作设置时才有意义。 因此,这些测试只是单独的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)

你可以从:

test_order = ['base']

def index_of(item, list):
    try:
        return list.index(item)
    except:
        return len(list) + 1

第二个定义命令 function:

def order_methods(x, y):
    x_rank = index_of(x[5:100], test_order)
    y_rank = index_of(y[5:100], test_order)
    return (x_rank > y_rank) - (x_rank < y_rank)

第三次将其设置在 class 中:

class ClassTests(unittest.TestCase):
    unittest.TestLoader.sortTestMethodsUsing = staticmethod(order_methods)

@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)

我还想为我的测试指定一个特定的执行顺序。 与此处其他答案的主要区别是:

  • 我想改写一个更冗长的测试方法名称,而不用step1step2等替换整个名称。
  • 我还希望控制台中的打印方法执行与在其他一些答案中使用单体解决方案相比具有一定的粒度。

所以对于单片测试方法的执行看起来像这样:

test_booking (__main__.TestBooking) ... ok

我想了:

test_create_booking__step1 (__main__.TestBooking) ... ok
test_process_booking__step2 (__main__.TestBooking) ... ok
test_delete_booking__step3 (__main__.TestBooking) ... ok

如何实现这一目标

例如,我使用__step<order>为我的方法名称提供了后缀(定义的顺序并不重要)

def test_create_booking__step1(self):
    [...]

def test_delete_booking__step3(self):
    [...]

def test_process_booking__step2(self):
    [...]

对于测试套件,覆盖__iter__ function 它将为测试方法构建一个迭代器。

class BookingTestSuite(unittest.TestSuite):
    """ Extends the functionality of the the standard test suites """
    
    def __iter__(self):
        for suite in self._tests:
            suite._tests = sorted(
                [x for x in suite._tests if hasattr(x, '_testMethodName')], 
                key = lambda x: int(x._testMethodName.split("step")[1])
            )
        return iter(self._tests)

这会将测试方法排序并相应地执行它们。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM