Python unittest.TestCase执行顺序
- 2025-04-17 09:02:00
- admin 原创
- 19
问题描述:
Python 中有没有办法unittest
设置测试用例的运行顺序?
在我目前的TestCase
课程中,一些测试用例会产生副作用,从而为其他测试用例的正常运行设置条件。现在我意识到正确的做法是使用 来setUp()
完成所有与设置相关的操作,但我希望实现一种设计,让每个后续测试构建的状态比下一个测试能够使用的状态略多一些。我觉得这样更优雅。
class MyTest(TestCase):
def test_setup(self):
# Do something
def test_thing(self):
# Do something that depends on test_setup()
理想情况下,我希望测试按照它们在类中出现的顺序运行。目前看来,它们是按字母顺序运行的。
解决方案 1:
不要让它们成为独立的测试 - 如果您想要一个整体测试,请编写一个整体测试。
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)
解决方案 2:
针对此类期望,始终编写一个单体测试是一个好习惯。但是,如果你像我一样笨手笨脚,那么你可以简单地编写一些看起来丑陋的方法,按照字母顺序排列,以便它们按照 Python 文档unittest — 单元测试框架中提到的从 a 到 b 排序。
请注意,各种测试用例的运行顺序是通过根据字符串的内置顺序对测试函数名称进行排序来确定的
例子
def test_a_first():
print "1"
def test_b_next():
print "2"
def test_c_last():
print "3"
解决方案 3:
来自unittest — 单元测试框架,组织测试代码部分:
注意:各种测试的运行顺序是通过根据字符串的内置顺序对测试方法名称进行排序来确定的。
因此,只需确保test_setup
的名称具有最小的字符串值。
请注意,您不应依赖此行为——不同的测试函数应该与执行顺序无关。如果您明确需要顺序,请参阅上面ngcohlan 的回答以获取解决方案。
解决方案 4:
我在任何相关问题中没有看到列出的另一种方法:使用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 方法列出任何几个“单元测试排序问题”(例如,这个问题和其他问题,包括执行顺序,或更改顺序,或测试顺序)。
解决方案 5:
我最终找到了一个适合我的简单解决方案:
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())
解决方案 6:
一种简单而灵活的方法是将比较器函数分配给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。
解决方案 7:
真正相互依赖的测试应该明确地链接到一个测试中。
需要不同级别设置的测试,也可以有相应的setUp()
运行足够的设置——各种方式都可以想象。
否则,unittest
默认按字母顺序处理测试类和测试类内的测试方法(即使loader.sortTestMethodsUsing
为 None)。dir()
在内部使用,按保证排序。
后一种行为可以用于提高实用性——例如,优先运行最新的工作测试,以加快编辑-测试-运行周期。但这种行为不应用于建立真正的依赖关系。请注意,测试可以通过命令行选项等单独运行。
解决方案 8:
一种方法是让这些子测试不被模块视为测试,方法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)
解决方案 9:
你可以从以下开始:
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)
解决方案 10:
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 += "
FAIL: {}
{} 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)
解决方案 11:
我还想为我的测试指定一个特定的执行顺序。这里与其他答案的主要区别是:
我想扭曲更详细的测试方法名称,而不使用等替换整个名称
step1
。step2
我还希望控制台中打印的方法执行具有一定的粒度,而不是在其他一些答案中使用单片解决方案。
因此,单片测试方法的执行如下所示:
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__
将为测试方法构建迭代器的函数。
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)
这将按顺序对测试方法进行排序并相应地执行它们。
扫码咨询,免费领取项目管理大礼包!