我正在用Python编写一个小的作业调度器。调度程序可以被赋予一系列可调用对象和依赖项,并且应该运行这些可调用对象,确保没有任务在其前一个任务之前运行。
我正在尝试遵循测试驱动的方法,并且我在测试依赖处理时遇到了一个问题。我的测试代码看起来像这样:
def test_add_dependency(self):
"""Tasks can be added with dependencies"""
# TODO: Unreliable test, may work sometimes because by default, task
# running order is indeterminate.
self.done = []
def test(id):
self.done.append("Test " + id)
s = Schedule()
tA = Task("Test A", partial(test, "A"))
tB = Task("Test B", partial(test, "B"))
s.add_task(tA)
s.add_task(tB)
s.add_dependency(tA, tB)
s.run()
self.assertEqual(self.done, ["Test B", "Test A"])
问题是,这个测试(有时)甚至在我添加依赖处理代码之前就工作了。这是因为规范没有规定任务必须以特定的顺序运行。所以正确的顺序是一个完全有效的选择,即使依赖信息被忽略。
是否有一种方法来编写测试以避免这种"意外"的成功?在我看来,这是一种相当常见的情况,特别是当采用测试驱动的"不编写没有失败测试的代码"方法时。你就像每个研究人员一样,看着一堆不完美的数据,试图说出关于它的假设是真的还是假的。
如果运行之间的结果不同,那么重新运行多次将给您一个样本,您可以应用统计来确定它是否工作。然而,如果一批运行会给你类似的结果,但在不同的日子里不同的一批运行会给你不同的结果,那么你的非确定性依赖于程序本身之外的事件,你需要找到一种方法来控制它们,理想情况下,这样它们就能最大限度地提高出错算法的几率。
这是不确定性的代价;你必须求助于统计数据,你必须得到正确的统计数据。你需要能够以某种置信度接受假设,同时拒绝零假设。如果你能最大化结果的方差,这就需要更少的样本;有不同的CPU负载,或者IO中断,或者调度一个随机休眠的任务。
对于定义一个有价值的测试来说,找出这样一个调度器受什么影响可能是明智的。
这个策略在大多数情况下是有效的:
首先,消除任何外部熵源(将线程池设置为使用单个线程;用预先播种的prng等模拟任何rng)然后,重复练习您的测试以产生输出的每种组合,仅更改被测机器的输入:
from itertools import permutations
def test_add_dependency(self):
"""Tasks can be added with dependencies"""
for p in permutations("AB"):
self.done = []
def test(id):
self.done.append("Test " + id)
s = Schedule(threads=1)
tasks = {id: Task("Test " + id, partial(test, id)) for id in "AB"}
s.add_task(tasks['A'])
s.add_task(tasks['B'])
s.add_dependency(tasks[p[0]], tasks[p[1]])
s.run()
self.assertEqual(self.done, ["Test " + p[1], "Test " + p[0]])
如果Schedule
未能使用来自add_dependency
的信息,则此测试将失败,因为这是在测试运行之间不同的熵(即信息)的唯一来源。
我建议您在编写测试之前确定需要测试的内容。
在上面的代码示例中,正在测试的是由调度程序生成的特定任务序列,尽管根据您对调度程序的描述,实际序列是不确定的,因此测试并没有真正提供任何关于代码的保证:有时它会通过,有时不会,当它通过时,它只是偶然的。
另一方面,更有价值的测试可能是断言结果中存在(或不存在)任务,而不断言它们的位置:"is in set" vs "is at array position"一种选择是为测试目的使用不同的、确定的Schedule
类版本(或添加一个选项以使现有版本具有确定性),但这可能会破坏单元测试的目的。
另一个选择是不为不确定的结果编写测试用例。
总的来说,你的问题的答案是……
是否有一种方法来编写测试以避免这种"意外"的成功?
…可能是"不",除了在写的时候特别警惕。尽管如果您有足够的警惕来避免编写有问题的测试用例,并且您将这种警惕应用于首先编写的代码,那么,可以说,您甚至不需要单元测试。: -)
如果单元测试的目的是检测代码中的错误,那么您如何检测单元测试中的错误?
你可以为你的单元测试编写"元"单元测试,但是你如何检测"元"单元测试中的bug呢?等等…
现在,这并不是说单元测试没有用处,但是单独来看,它们不足以"证明"代码是"正确的"。在实践中,我发现基于同行的代码审查是一种更有效的检测代码缺陷的方法。