Python unittest.TestCase执行顺序


77

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

在我当前的TestCase课程中,一些测试用例具有一些副作用,这些条件为其他测试用例的正常运行设置了条件。现在,我意识到执行此操作的正确方法是使用它setUp()来完成所有设置工作,但是我想实现一个设计,其中每个连续的测试都会建立更多下一个可以使用的状态。我觉得这更优雅。

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

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

Answers:


71

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

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 2015年

1
注意,在您的代码sorted()中并不是真正必要的,因为dir()返回的步骤方法是按担保字母顺序排序的。这就是为什么unittest默认情况下(即使sortTestMethodsUsing是None时)也按字母顺序处理测试类和测试方法的原因-可以利用该类以提高实用性,例如首先运行最新工作测试以加快edit-testrun-cycle的速度。
kxr

1
@ncoghlan Nick,只想感谢您对测试的这些评论-确实使我对自己遇到的问题大开眼界。我也跟踪了您的其他一些同样出色的答案。干杯!
布兰登·贝特尔森

40

始终为这种期望编写整体测试是一个好习惯,但是,如果像我这样愚蠢的家伙,那么您可以简单地按字母顺序编写难看的方法,以便按照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"

4
IMO这种方法比添加更多代码作为解决方法更好。
猛禽

您为什么说编写整体测试是一种好习惯?查看Java TestNG通过测试组和依赖项执行的更复杂的方法。无论如何,我也是一个愚蠢的人,当我以alpha顺序编写测试时,我发现通过全局变量传递状态很有用,因为测试运行器可能为每个测试创建不同的实例。
约书亚·理查森

1
@Joshua就像其他所有东西一样,没有“一个解决方案可以统统解决”,Monolithinc解决方案通常是一些程序员考虑的良好设计实践,有序测试或方案驱动的测试违反了其中一项测试设计规则。是“每个期望一个测试”,但您不必遵守这一要求。我不是Java的忠实拥护者,仅因为框架尝试执行某项操作并不一定意味着其良好实践。测试小组这个词本身对我没有意义,但是可以随意做任何事情。
varun

我认为有一个测试应该运行在最后而不是中间,因此它将以“ z”开头。
卡达姆

26

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

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

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

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


6
不同的测试运行者,不同的行为。您的建议对编写稳定的代码snd测试没有帮助。
Andreas Jung

20

旧问题,但在其他相关问题中未列出的另一种方法:使用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方法列出了几个“单元测试排序问题”中的任何一个(例如,这个问题以及其他一些问题,包括执行顺序更改顺序测试顺序)。


这很好,除了它会为每个测试用例创建一个新类。有没有办法保留来自test_ten的数据并在test_eleven中使用它?
唐卡

@thang如果您制作东西,@classmethod那么它们可以在实例之间保持状态。
尼克·查普曼

在执行此操作时,您是否知道setUpClass被调用?还是需要手动运行?
尼克·查普曼

@NickChapman这有什么意义?@类方法几乎使它成为一个静态函数(类信息作为参数)

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为None时)。dir()在内部使用,以保证排序。

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


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模块的排序顺序要好,因为明天可能会改变,并且按顺序实现拓扑排序也不会很简单。

从此处获取 此方法的示例(免责声明:我自己的模块),如下所示。

在这里,测试用例运行独立的测试,例如检查表参数not set(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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.