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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容