Python之学会测试,让开发更加高效

        前几天,听了公司某位大佬关于编程心得的体会,其中讲到了“测试驱动开发”,感觉自己的测试技能薄弱,因此,写下这篇文章,希望对测试能有个入门。这段时间,笔者也体会到了测试的价值,一句话,学会测试,能够让你的开发更加高效。

本文将介绍以下两个方面的内容:

Test with Coverage

Mock

Test with Coverage

测试覆盖率通常被用来衡量测试的充分性和完整性。从广义的角度讲,主要分为两大类:面向项目的需求覆盖率和更偏向技术的代码覆盖率。对于开发人员来说,我们更注重代码覆盖率。

代码覆盖率指的是至少执行了一次的条目数占整个条目数的百分比。如果条目数是语句,对应的就是代码行覆盖率;如果条目数是函数,对应的就是函数覆盖率;如果条目数是路径,对应的就是路径覆盖率,等等。统计代码覆盖率的根本目的是找出潜在的遗漏测试用例,并有针对性的进行补充,同时还可以识别出代码中那些由于需求变更等原因造成的废弃代码。通常我们希望代码覆盖率越高越好,代码覆盖率越高越能说明你的测试用例设计是充分且完备的,但测试的成本会随着代码覆盖率的提高而增加。

在Python中,coverage模块帮助我们实现了代码行覆盖率,我们可以方便地使用它来完整测试的代码行覆盖率。

我们通过一个例子来介绍coverage模块的使用。

首先,我们有脚本func_add.py,实现了add函数,代码如下:

# -*- coding: utf-8 -*-

defadd(a, b):

ifisinstance(a, str)andisinstance(b, str):

returna +'+'+ b

elifisinstance(a, list)andisinstance(b, list):

returna + b

elifisinstance(a, (int, float))andisinstance(b, (int, float)):

returna + b

else:

returnNone

在add函数中,分四种情况实现了加法,分别是字符串,列表,属性值,以及其它情况。

接着,我们用unittest模块来进行单元测试,代码脚本(test_func_add.py)如下:

importunittest

fromfunc_addimportadd

classTest_Add(unittest.TestCase):

defsetUp(self):

pass

deftest_add_case1(self):

a ="Hello"

b ="World"

res = add(a, b)

print(res)

self.assertEqual(res,"Hello+World")

deftest_add_case2(self):

a =1

b =2

res = add(a, b)

print(res)

self.assertEqual(res,3)

deftest_add_case3(self):

a = [1,2]

b = [3]

res = add(a, b)

print(res)

self.assertEqual(res, [1,2,3])

deftest_add_case4(self):

a =2

b ="3"

res = add(a, b)

print(None)

self.assertEqual(res,None)

if__name__ =='__main__':

# 部分用例测试

# 构造一个容器用来存放我们的测试用例

suite = unittest.TestSuite()

# 添加类中的测试用例

suite.addTest(Test_Add('test_add_case1'))

suite.addTest(Test_Add('test_add_case2'))

# suite.addTest(Test_Add('test_add_case3'))

# suite.addTest(Test_Add('test_add_case4'))

run = unittest.TextTestRunner()

run.run(suite)

在这个测试中,我们只测试了前两个用例,也就是对字符串和数值型的加法进行测试。

在命令行中输入coverage run test_func_add.py命令运行该测试脚本,输出结果如下:

Hello+World

.3

.

----------------------------------------------------------------------

Ran2testsin0.000s

OK

再输入命令coverage html就能生成代码行覆盖率的报告,会生成htmlcov文件夹,打开其中的index.html文件,就能看到本次执行的覆盖率情况,如下图:

测试覆盖率结果总览

我们点击func_add.py查看add函数测试的情况,如下图:

func_add.py脚本的测试覆盖率情况

可以看到,单元测试脚本test_func_add.py的前两个测试用例只覆盖到了add函数中左边绿色的部分,而没有测试到红色的部分,代码行覆盖率为75%。

  因此,还有两种情况没有覆盖到,说明我们的单元测试中的测试用例还不够充分。

  在test_func_add.py中,我们把main函数中的注释去掉,把后两个测试用例也添加进来,这时候我们再运行上面的coverage模块的命令,重新生成htmlcov后,func_add.py的代码行覆盖率如下图:

增加测试用例后,func_add.py脚本的测试覆盖率情况

  可以看到,增加测试用例后,我们调用的add函数代码行覆盖率为100%,所有的代码都覆盖到了。

Mock

Mock这个词在英语中有模拟的这个意思,因此我们可以猜测出这个库的主要功能是模拟一些东西。准确的说,Mock是Python中一个用于支持单元测试的库,它的主要功能是使用mock对象替代掉指定的Python对象,以达到模拟对象的行为。在Python3中,mock是辅助单元测试的一个模块。它允许您用模拟对象替换您的系统的部分,并对它们已使用的方式进行断言。

在实际生产中的项目是非常复杂的,对其进行单元测试的时候,会遇到以下问题:

接口的依赖

外部接口调用

测试环境非常复杂

单元测试应该只针对当前单元进行测试, 所有的内部或外部的依赖应该是稳定的, 已经在别处进行测试过的。使用mock 就可以对外部依赖组件实现进行模拟并且替换掉, 从而使得单元测试将焦点只放在当前的单元功能。

我们通过一个简单的例子来说明mock模块的使用。

首先,我们有脚本mock_multipy.py,主要实现的功能是Operator类中的multipy函数,在这里我们可以假设该函数并没有实现好,只是存在这样一个函数,代码如下:

# -*- coding: utf-8 -*-

# mock_multipy.py

classOperator():

defmultipy(self, a, b):

pass

尽管我们没有实现multipy函数,但是我们还是想对这个函数的功能进行测试,这时候我们可以借助mock模块中的Mock类来实现。测试的脚本(mock_example.py)代码如下:

# -*- coding: utf-8 -*-

fromunittestimportmock

importunittest

frommock_multipyimportOperator

# test Operator class

classTestCount(unittest.TestCase):

deftest_add(self):

op = Operator()

# 利用Mock类,我们假设返回的结果为15

op.multipy = mock.Mock(return_value=15)

# 调用multipy函数,输入参数为4,5,实际并未调用

result = op.multipy(4,5)

# 声明返回结果是否为15

self.assertEqual(result,15)

if__name__ =='__main__':

unittest.main()

让我们对上述的代码做一些说明。

op.multipy = mock.Mock(return_value=15)

通过Mock类来模拟调用Operator类中的multipy()函数,return_value 定义了multipy()方法的返回值。

result = op.multipy(4,5)

result值调用multipy()函数,输入参数为4,5,但实际并未调用,最后通过assertEqual()方法断言,返回的结果是否是预期的结果为15。输出的结果如下:

Ran 1testin0.002s

OK

通过Mock类,我们即使在multipy函数并未实现的情况下,仍然能够通过想象函数执行的结果来进行测试,这样如果有后续的函数依赖multipy函数,也并不影响后续代码的测试。

利用Mock模块中的patch函数,我们可以将上述测试的脚本代码简化如下:

# -*- coding: utf-8 -*-

importunittest

fromunittest.mockimportpatch

frommock_multipyimportOperator

# test Operator class

classTestCount(unittest.TestCase):

@patch("mock_multipy.Operator.multipy")

deftest_case1(self, tmp):

tmp.return_value =15

result = Operator().multipy(4,5)

self.assertEqual(15, result)

if__name__ =='__main__':

unittest.main()

patch()装饰器可以很容易地模拟类或对象在模块测试。在测试过程中,您指定的对象将被替换为一个模拟(或其他对象),并在测试结束时还原。

那如果我们后面又实现了multipy函数,是否仍然能够测试呢?

修改mock_multipy.py脚本,代码如下:

# -*- coding: utf-8 -*-

# mock_multipy.py

classOperator():

defmultipy(self, a, b):

returna * b

这时候,我们再运行mock_example.py脚本,测试仍然通过,这是因为multipy函数返回的结果仍然是我们mock后返回的值,而并未调用真正的Operator类中的multipy函数。

我们修改mock_example.py脚本如下:

# -*- coding: utf-8 -*-

fromunittestimportmock

importunittest

frommock_multipyimportOperator

# test Operator class

classTestCount(unittest.TestCase):

deftest_add(self):

op = Operator()

# 利用Mock类,添加side_effect参数

op.multipy = mock.Mock(return_value=15, side_effect=op.multipy)

# 调用multipy函数,输入参数为4,5,实际已调用

result = op.multipy(4,5)

# 声明返回结果是否为15

self.assertEqual(result,15)

if__name__ =='__main__':

unittest.main()

side_effect参数和return_value参数是相反的。它给mock分配了可替换的结果,覆盖了return_value。简单的说,一个模拟工厂调用将返回side_effect值,而不是return_value。所以,设置side_effect参数为Operator类中的multipy函数,那么return_value的作用失效。

运行修改后的测试脚本,测试结果如下:

Ran 1testin0.004s

FAILED (failures=1)

15 != 20

Expected :20

Actual   :15

可以发现,multipy函数返回的值为20,不等于我们期望的值15,这是side_effect函数的作用结果使然,返回的结果调用了Operator类中的multipy函数,所以返回值为20。

在self.assertEqual(result, 15)中将15改成20,运行测试结果如下:

Ran 1testin0.002s

OK

  本次分享到此结束,感谢大家的阅读~

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