Python中的单元测试

image
  • 来源 | 愿码(ChainDesk.CN)内容编辑
  • 愿码Slogan | 连接每个程序员的故事
  • 网站 | http://chaindesk.cn
  • 愿码愿景 | 打造全学科IT系统免费课程,助力小白用户、初级工程师0成本免费系统学习、低成本进阶,帮助BAT一线资深工程师成长并利用自身优势创造睡后收入。
  • 官方公众号 | 愿码 | 愿码服务号 | 区块链部落
  • 免费加入愿码全思维工程师社群 | 任一公众号回复“愿码”两个字获取入群二维码

本文阅读时长:11min

基本单元测试


在我们开始讨论新的概念和功能之前,让我们来看看如何使用unittest来表达我们已经学到的想法。这样,我们就能有一些坚实的基础来建立我们的新理解。

采取行动的时间-用unittest测试PID


我们将访问PID类(或至少访问PID类的测试)。我们将编写测试,以便它们在unittest框架内运行。

我们将使用unittest框架实现测试。

  1. 创建一个名为新文件test_pid.py在同一目录pid.py。请注意,这是一个.py文件:unittest测试是纯 python源代码,而不是包含源代码的纯文本。这意味着从纪录片的角度来看,测试的用处不大,但可以交换其他好处。

  2. 将以下代码插入到新创建的test_pid.py中

from unittest import TestCase, main
from mocker import Mocker
import pid
class test_pid_constructor(TestCase):
 def test_without_when(self):
 mocker = Mocker()
 mock_time = mocker.replace('time.time')
 mock_time()
 mocker.result(1.0)
 mocker.replay()
 controller = pid.PID(P=0.5, I=0.5, D=0.5,
 setpoint=0, initial=12)
 mocker.restore()
 mocker.verify()
 self.assertEqual(controller.gains, (0.5, 0.5, 0.5))
 self.assertAlmostEqual(controller.setpoint[0], 0.0)
 self.assertEqual(len(controller.setpoint), 1)
 self.assertAlmostEqual(controller.previous_time, 1.0)
 self.assertAlmostEqual(controller.previous_error, -12.0)
 self.assertAlmostEqual(controller.integrated_error, 0)
 def test_with_when(self):
 controller = pid.PID(P=0.5, I=0.5, D=0.5,
 setpoint=1, initial=12,
 when=43)
 self.assertEqual(controller.gains, (0.5, 0.5, 0.5))
 self.assertAlmostEqual(controller.setpoint[0], 1.0)
 self.assertEqual(len(controller.setpoint), 1)
 self.assertAlmostEqual(controller.previous_time, 43.0)
 self.assertAlmostEqual(controller.previous_error, -11.0)
 self.assertAlmostEqual(controller.integrated_error, 0)
class test_calculate_response(TestCase):
 def test_without_when(self):
 mocker = Mocker()
 mock_time = mocker.replace('time.time')
 mock_time()
 mocker.result(1.0)
 mock_time()
 mocker.result(2.0)
 mock_time()
 mocker.result(3.0)
 mock_time()
 mocker.result(4.0)
 mock_time()
 mocker.result(5.0)
 mocker.replay()
 controller = pid.PID(P=0.5, I=0.5, D=0.5,
 setpoint=0, initial=12)
 self.assertEqual(controller.calculate_response(6), -3)
 self.assertEqual(controller.calculate_response(3), -4.5)
 self.assertEqual(controller.calculate_response(-1.5), -0.75)
 self.assertEqual(controller.calculate_response(‑2.25), 
‑1.125)
 mocker.restore()
 mocker.verify()
 def test_with_when(self):
 controller = pid.PID(P=0.5, I=0.5, D=0.5,
 setpoint=0, initial=12,
 when=1)
 self.assertEqual(controller.calculate_response(6, 2), -3)
 self.assertEqual(controller.calculate_response(3, 3), -4.5)
 self.assertEqual(controller.calculate_response(‑1.5, 4), 
‑0.75)
 self.assertEqual(controller.calculate_response(‑2.25, 5), 
‑1.125)
if __name__ == '__main__':
 main()
  1. 键入以下命令运行测试:$ python test_pid.py
image

让我们浏览代码部分,看看每个部分的作用。

from unittest import TestCase, main
from mocker import Mocker
import pid
class test_pid_constructor(TestCase):
 def test_without_when(self):
 mocker = Mocker()
 mock_time = mocker.replace('time.time')
 mock_time()
 mocker.result(1.0)
 mocker.replay()
 controller = pid.PID(P=0.5, I=0.5, D=0.5,
 setpoint=0, initial=12)
 mocker.restore()
 mocker.verify()
 self.assertEqual(controller.gains, (0.5, 0.5, 0.5))
 self.assertAlmostEqual(controller.setpoint[0], 0.0)
 self.assertEqual(len(controller.setpoint), 1)
 self.assertAlmostEqual(controller.previous_time, 1.0)
 self.assertAlmostEqual(controller.previous_error, -12.0)
 self.assertAlmostEqual(controller.integrated_error, 0)

在一些设置代码之后,我们进行了测试,当没有给出when参数时,PID控制器正常工作。Mocker用于将time.time替换为始终返回可预测值的模拟,然后我们使用多个断言来确认控制器的属性已初始化为预期值。

def test_with_when(self):
 controller = pid.PID(P=0.5, I=0.5, D=0.5,
 setpoint=1, initial=12,
 when=43)
 self.assertEqual(controller.gains, (0.5, 0.5, 0.5))
 self.assertAlmostEqual(controller.setpoint[0], 1.0)
 self.assertEqual(len(controller.setpoint), 1)
 self.assertAlmostEqual(controller.previous_time, 43.0)
 self.assertAlmostEqual(controller.previous_error, -11.0)
 self.assertAlmostEqual(controller.integrated_error, 0)

此测试确认在提供when参数时PID构造函数正常工作。与之前的测试不同,不需要使用Mocker,因为测试的结果不应该依赖于除参数值之外的任何东西 - 当前时间是无关紧要的。

class test_calculate_response(TestCase):
 def test_without_when(self):
 mocker = Mocker()
 mock_time = mocker.replace('time.time')
 mock_time()
 mocker.result(1.0)
 mock_time()
 mocker.result(2.0)
 mock_time()
 mocker.result(3.0)
 mock_time()
 mocker.result(4.0)
 mock_time()
 mocker.result(5.0)
 mocker.replay()
 controller = pid.PID(P=0.5, I=0.5, D=0.5,
 setpoint=0, initial=12)
 self.assertEqual(controller.calculate_response(6), -3)
 self.assertEqual(controller.calculate_response(3), -4.5)
 self.assertEqual(controller.calculate_response(-1.5), -0.75)
 sel+f.assertEqual(controller.calculate_response(‑2.25), 
‑1.125)
 mocker.restore()
 mocker.verify()

此类中的测试描述了calculate_response方法的预期行为。第一个测试检查未提供可选的when参数时的行为,并模拟time.time以使该行为可预测。

def test_with_when(self):
 controller = pid.PID(P=0.5, I=0.5, D=0.5,
 setpoint=0, initial=12,
 when=1)
 self.assertEqual(controller.calculate_response(6, 2), -3)
 self.assertEqual(controller.calculate_response(3, 3), -4.5)
 self.assertEqual(controller.calculate_response(‑1.5, 4), 
‑0.75)
 self.assertEqual(controller.calculate_response(‑2.25, 5), 
‑1.125)

在此测试中,提供了when参数,因此无需模拟time.time。我们只需检查结果是否符合预期。

我们执行的实际测试与doctest中编写的测试相同。到目前为止,我们所看到的只是一种表达它们的不同方式。

首先要注意的是,测试文件被划分为继承自unittest.TestCase的类,每个类都包含一个或多个测试方法。每个测试方法的名称以单词test开头,单元测试是如何识别它们是测试的。

每种测试方法都包含对单个单元的单个测试。这为我们提供了一种方便的方法来构建我们的测试,将相关测试组合到同一个类中,以便更容易找到它们。

将每个测试放入自己的方法意味着每个测试都在一个独立的命名空间中执行,这使得相对于doctest风格的测试,使得单元测试式测试更容易相互干扰。它还意味着unittest知道测试文件中有多少单元测试,而不是简单地知道有多少表达式(您可能已经注意到doctest将每个>>>行作为单独的测试计数)。最后,将每个测试放在自己的方法中意味着每个测试都有一个名称,这可能是一个有价值的功能。

unittest中的测试并不直接关注任何不属于调用TestCase的assert方法的任何内容。这意味着当我们使用Mocker时,我们不必担心从演示表达式返回的模拟对象,除非我们想要使用它们。这也意味着我们需要记住写一个断言来描述我们想要检查的测试的每个方面。我们将很快介绍TestCase的各种断言方法。

如果您无法执行测试,则测试没有多大用处。目前,我们将采用的方式是通过Python解释器将测试文件作为程序执行时 调用unittest.main。这是运行unittest代码的最简单方法,但是当你在很多文件中分布了大量测试时,这很麻烦。

如果name =='__ main__':当 Python加载任何模块时,它将该模块的名称存储在模块中名为name的变量中(除非该模块是在命令行上传递给解释器的模块)。该模块始终将字符串'main'绑定到其name变量。因此,如果name =='__ main__':表示 - 如果此模块直接从命令行执行。

Assertions


Assertions是我们用来告诉unittest测试的重要结果是什么的机制。通过使用适当的断言,我们可以准确地告诉unittest每次测试的期望。

assertTrue


当我们调用self.assertTrue(expression)时,我们告诉unittest表达式必须为true才能使测试成功。

这是一个非常灵活的断言,因为您可以通过编写适当的布尔表达式来检查几乎任何内容。这也是你应该考虑使用的最后一个断言之一,因为它没有告诉unittest你正在进行的比较的类型,这意味着unittest无法清楚地告诉你如果测试失败会出现什么问题。

有关此示例,请考虑以下测试代码,其中包含两个保证失败的测试:

from unittest import TestCase, main
class two_failing_tests(TestCase):
 def test_assertTrue(self):
 self.assertTrue(1 == 1 + 1)
 def test_assertEqual(self):
 self.assertEqual(1, 1 + 1)
if __name__ == '__main__':
 main()

看起来两个测试似乎是可以互换的,因为两个测试都是相同的。当然他们都会失败(或者在不太可能的情况下,他们都会失败),所以为什么选择一个而不是另一个呢?

看看我们运行测试时会发生什么(并且还注意到测试没有按照它们编写的顺序执行;测试完全相互独立,所以没关系,对吧?):

image

你看得到差别吗?该assertTrue测试能够正确地确定测试失败,但它不知道够报告关于失败原因的任何有用的信息。该assertEqual便测试,而另一方面,他知道首先,它是检查两个表达式是相等的,其次它知道如何呈现的结果,因此,他们将是最有用的:通过评估各个它是表达的比较并在结果之间放置一个!=符号。它告诉我们什么期望失败,以及相关表达式评估的内容。

assertFalse


assertFalse方法会成功时assertTrue方法会失败,反之亦然。它在产生assertTrue所具有的有用输出方面具有相同的限制,并且在能够测试几乎任何条件方面具有相同的灵活性。

assertEqual


正如assertTrue讨论中所提到的,assertEqual断言检查它的两个参数实际上是相等的,并且如果它们不是,则报告失败,以及参数的实际值。

assertNotEqual


assertNotEqual每当断言失败assertEqual便断言会成功,反之亦然。报告失败时,其输出表明两个表达式的值相等,并为您提供这些值。

assertAlmostEqual


正如我们之前看到的,比较浮点数可能很麻烦。特别是,检查两个浮点数是否相等是有问题的,因为你可能期望相等的事情 - 在数学上是相等的 - 可能仍然最终在最低有效位之间不同。浮点数仅在每个位相同时才相等。

为了解决这个问题,unittest提供了assertAlmostEqual,它检查两个浮点值是否几乎相同; 它们之间的少量差异是可以容忍的。

让我们看一下这个问题。如果取平方根7,然后将其平方,则结果应为7.这是一对检查该事实的测试:

from unittest import TestCase, main
class floating_point_problems(TestCase):
 def test_assertEqual(self):
 self.assertEqual((7.0 ** 0.5) ** 2.0, 7.0)
def test_assertAlmostEqual(self): 
 self.assertAlmostEqual((7.0 ** 0.5) ** 2.0, 7.0) 
if __name__ == '__main__': 
 main()  

test_assertEqual方法检查

image

这在现实中是如此。然而,在计算机可用的更专业的数字系统中,取7的平方根然后平方它并不能让我们回到7,所以这个测试将失败。稍等一下。

测试test_assertAlmostEqual方法检查

image

即使计算机会同意这是真的,所以这个测试应该通过。

运行这些测试会产生以下结果,尽管您返回的具体数字可能会有所不同,具体取决于运行测试的计算机的详细信息:

image

不幸的是,浮点数不精确,因为实数行上的大多数数字不能用有限的,非重复的数字序列表示,更不用说仅仅64位。因此,你从评估数学表达式得到的回报并不是很好。虽然 - 或者几乎任何其他类型的工作都足够接近政府工作 - 所以我们不希望我们的测试对这个微小的差异进行狡辩。因此,当我们比较浮点数是否相等时,我们应该使用assertAlmostEqualassertNotAlmostEqual

这个问题通常不会延续到其他比较运算符中。例如,检查一个浮点数小于另一个,由于无意义的错误,不太可能产生错误的结果。只有在平等的情况下,这个问题才会困扰我们。

你可能感兴趣的:(Python中的单元测试)