Python的unittest.TestCase执行顺序

96

在Python的unittest中有没有一种方法可以设置测试用例运行的顺序?

在我的TestCase类中,一些测试用例具有副作用,可以设置其他条件以便其正确运行。现在我意识到正确的做法是使用setUp()来完成所有设置相关的事情,但我想实现一种设计,在这种设计中,每个后续的测试都会构建稍微多一点状态供下一个测试使用。我认为这更加优雅。

class MyTest(TestCase):

  def test_setup(self):
    # Do something

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

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

11个回答

89

不要把它们作为相互独立的测试 - 如果你想要一个整体化的测试,就编写一个整体化的测试。

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
(在Python 3.4之前的版本中,可以通过unittest2来使用“subtests”特性:https://pypi.python.org/pypi/unittest2

我对单元测试很陌生,但我感觉过于庞大的测试是不好的。这是真的吗?我刚创建了我的测试套件,并且非常依赖使用你的代码来进行庞大的测试。这是表示我采用了错误的单元测试方法的迹象吗?谢谢。 - swdev
12
纯单元测试的好处在于,当它们失败时,通常会准确地告诉您出了什么问题。当您尝试修复它们时,还可以只重新运行失败的测试。像这样的整体测试就没有这些好处:当它们失败时,需要进行调试以找出问题所在。另一方面,像这样的测试通常更容易、更快速地编写,特别是在对不考虑单元测试的现有应用程序进行测试时。 - ncoghlan
请使用更好的命名约定来命名测试名称,例如“test_1_newuser”和“test_2_signin”。这样做是因为测试会按照字符串的内置排序进行排序。 - shakirthow
6
如果执行顺序很重要,那么这些测试就不再是单元测试了,它们成为了场景测试中的步骤。这仍然是一个有价值的事情,但最好将其处理为像示例中展示的较大的测试用例,或者使用更高级别的行为测试框架,比如 http://pythonhosted.org/behave/。 - ncoghlan
1
请注意,在您的代码中,sorted()并不是必需的,因为dir()按字母顺序保证返回步骤方法。这也是为什么unittest默认按字母顺序处理测试类和测试方法(即使sortTestMethodsUsing为None)- 这可以用于实用性,例如让最新的工作测试首先运行,以加快编辑 - 测试运行周期。 - kxr
1
@ncoghlan Nick,想感谢你在测试方面的评论 - 真的让我对我遇到的问题有了新的认识。我还偷偷看了一些你其他的回答,同样非常出色。干杯! - Brandon Bertelsen

60

对于这样的期望,编写完整的单元测试是一个好的实践。然而,如果你像我一样很怪,那么你可以简单地按字母顺序编写难看的方法,使它们按照Python文档中提到的从a到b排序 - unittest — 单元测试框架

请注意,各个测试用例运行的顺序取决于将测试函数名称根据字符串的内置排序进行排序

示例

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

8
这种方法比添加更多代码来解决问题更好。 - Raptor
为什么说编写单体测试是一种好的实践方法?可以查看更复杂的Java TestNG方式,它使用测试组和依赖项。无论如何,我也是一个愚蠢的人,当我按字母顺序编写测试时,我发现通过全局变量传递状态很有用,因为测试运行程序可能为每个测试创建不同的实例。 - Joshua Richardson
1
@Joshua 就像其他所有事物一样,没有“一种解决方案统治它们所有”的方法,单体架构解决方案通常被一些程序员视为良好的设计实践,有序测试或场景驱动测试打破了测试的一个设计规则,即“每个期望一个测试”,但您不必遵守此规则。我不是一个大的Java粉丝,仅仅因为一个框架试图做某事并不意味着它是一个好的实践。而且,“测试组”这个词对我来说没有意义,但随便你怎么做吧。 - varun
我在考虑一项测试应该在最后而不是在中间运行,因此将其名称以“z”开头。 - cardamom


27

还有一种方法没有在相关问题中列出:使用 TestSuite

另一种实现测试用例排序的方法是将测试用例添加到 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方法的列表(例如,这个问题和其他一些问题,包括执行顺序,或改变顺序,或测试顺序)。


@thang 如果你将东西标记为 @classmethod,那么它们可以在实例之间保持状态。 - Nick Chapman
在执行此操作时,您是否知道 setUpClass 是否被调用?或者它需要手动运行? - Nick Chapman
1
@thang @classmethod != @staticmethod!!! 要小心,它们是完全不同的东西。@staticmethod 允许您在没有类实例的情况下调用方法。@classmethod 让您可以访问类和类本身上可以存储信息。例如,如果您在类方法中执行 cls.somevar = 10,那么在运行该函数后,该类的所有实例和所有其他类方法都将看到 somevar = 10。类本身是可以绑定值的对象。 - Nick Chapman
@NickChapman 是的,这基本上使它成为一个带有类对象作为参数的静态方法。您也可以不使用任何实例调用 @classmethod。假设我在同一类上有两组测试。在第一组中,我想共享一个实例,在第二组中,我想共享第二个实例。这是不可能的。如果我有一个名为 C 的类,其中包含测试函数 t1、t2、t3、t4。我想这样做:x1 = C(),x1.t1(),x1.t3(),x2 = C(),x2.t1(),x2.t2(),x2.t4()。 - thang
@thang 我实际上就是做了类似的事情。为了解决这个问题,我把所有的测试都放在一个包装类中,然后让这个包装类向每个测试类注入自己的实例,以创建一种共享总线来连接这些测试。 - Nick Chapman
显示剩余6条评论

6
我最终采用了一个对我有效的简单解决方案:

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

5
一种简单而灵活的方式是将比较函数分配给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版本。


2

那些互相依赖的测试应该明确地链接在一个测试中。

那些需要不同级别设置的测试也可以拥有它们对应的setUp()方法来运行足够的设置 - 可以考虑多种方式。

否则,unittest默认按字母顺序处理测试类和测试类中的测试方法 (即使loader.sortTestMethodsUsing为 None)。内部使用dir()进行排序。

后一种行为可以用于提高实用性 - 例如,让最新的工作测试先运行以加快编辑 - 测试运行周期。但是,这种行为不应该用于建立真正的依赖关系。请注意,测试可以通过命令行选项等单独运行。


1
你可以从以下内容开始:
test_order = ['base']

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

第二步,定义排序函数:

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 ClassTests(unittest.TestCase):
    unittest.TestLoader.sortTestMethodsUsing = staticmethod(order_methods)

1
一种方法是在子测试名称前加下划线(_),以使它们不被 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)

是的,但您会冒着假阴性测试的风险:这些测试存在,但由于您意外地遗漏了它们而未被执行。例如,如果您在test_CRUD中意外遗漏了对self._test_UPDATE()的调用。 - Peter Mortensen

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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接