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

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

  • Test with Coverage
  • Mock

Test with Coverage

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

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

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

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

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

  1. # -*- coding: utf-8 -*-
  2. def add(a, b):
  3. if isinstance(a, str) and isinstance(b, str):
  4. return a + '+' + b
  5. elif isinstance(a, list) and isinstance(b, list):
  6. return a + b
  7. elif isinstance(a, (int, float)) and isinstance(b, (int, float)):
  8. return a + b
  9. else:
  10. return None

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

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

  1. import unittest
  2. from func_add import add
  3. class Test_Add(unittest.TestCase):
  4. def setUp(self):
  5. pass
  6. def test_add_case1(self):
  7. a = "Hello"
  8. b = "World"
  9. res = add(a, b)
  10. print(res)
  11. self.assertEqual(res, "Hello+World")
  12. def test_add_case2(self):
  13. a = 1
  14. b = 2
  15. res = add(a, b)
  16. print(res)
  17. self.assertEqual(res, 3)
  18. def test_add_case3(self):
  19. a = [1, 2]
  20. b = [3]
  21. res = add(a, b)
  22. print(res)
  23. self.assertEqual(res, [1, 2, 3])
  24. def test_add_case4(self):
  25. a = 2
  26. b = "3"
  27. res = add(a, b)
  28. print(None)
  29. self.assertEqual(res, None)
  30. if __name__ == '__main__':
  31. # 部分用例测试
  32. # 构造一个容器用来存放我们的测试用例
  33. suite = unittest.TestSuite()
  34. # 添加类中的测试用例
  35. suite.addTest(Test_Add('test_add_case1'))
  36. suite.addTest(Test_Add('test_add_case2'))
  37. # suite.addTest(Test_Add('test_add_case3'))
  38. # suite.addTest(Test_Add('test_add_case4'))
  39. run = unittest.TextTestRunner()
  40. run.run(suite)

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

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

  1. Hello+World
  2. .3
  3. .
  4. ----------------------------------------------------------------------
  5. Ran 2 tests in 0.000s
  6. OK

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



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



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

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

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



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

Mock

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

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

  • 接口的依赖
  • 外部接口调用
  • 测试环境非常复杂

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

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

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

  1. # -*- coding: utf-8 -*-
  2. # mock_multipy.py
  3. class Operator():
  4. def multipy(self, a, b):
  5. pass

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

  1. # -*- coding: utf-8 -*-
  2. from unittest import mock
  3. import unittest
  4. from mock_multipy import Operator
  5. # test Operator class
  6. class TestCount(unittest.TestCase):
  7. def test_add(self):
  8. op = Operator()
  9. # 利用Mock类,我们假设返回的结果为15
  10. op.multipy = mock.Mock(return_value=15)
  11. # 调用multipy函数,输入参数为4,5,实际并未调用
  12. result = op.multipy(4, 5)
  13. # 声明返回结果是否为15
  14. self.assertEqual(result, 15)
  15. if __name__ == '__main__':
  16. unittest.main()

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

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

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

  1. result = op.multipy(4, 5)

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

  1. Ran 1 test in 0.002s
  2. OK

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

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

  1. # -*- coding: utf-8 -*-
  2. import unittest
  3. from unittest.mock import patch
  4. from mock_multipy import Operator
  5. # test Operator class
  6. class TestCount(unittest.TestCase):
  7. @patch("mock_multipy.Operator.multipy")
  8. def test_case1(self, tmp):
  9. tmp.return_value = 15
  10. result = Operator().multipy(4, 5)
  11. self.assertEqual(15, result)
  12. if __name__ == '__main__':
  13. unittest.main()

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

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

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

  1. # -*- coding: utf-8 -*-
  2. # mock_multipy.py
  3. class Operator():
  4. def multipy(self, a, b):
  5. return a * b

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

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

  1. # -*- coding: utf-8 -*-
  2. from unittest import mock
  3. import unittest
  4. from mock_multipy import Operator
  5. # test Operator class
  6. class TestCount(unittest.TestCase):
  7. def test_add(self):
  8. op = Operator()
  9. # 利用Mock类,添加side_effect参数
  10. op.multipy = mock.Mock(return_value=15, side_effect=op.multipy)
  11. # 调用multipy函数,输入参数为4,5,实际已调用
  12. result = op.multipy(4, 5)
  13. # 声明返回结果是否为15
  14. self.assertEqual(result, 15)
  15. if __name__ == '__main__':
  16. unittest.main()

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

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

  1. Ran 1 test in 0.004s
  2. FAILED (failures=1)
  3. 15 != 20
  4. Expected :20
  5. Actual :15

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

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

  1. Ran 1 test in 0.002s
  2. OK

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

Python之学会测试,让开发更加高效(一)的更多相关文章

  1. testNG优雅的使用注解让你的测试项目开发更高效!

    testNG大部分是通过xml配置测试类和监听类 但是这种方法就像传统的spring框架一样需要引入大量的xml配置信息,而且在各层之间也需要通过new对象传递.如果testNG能使用注解注入bean ...

  2. (数据科学学习手札116)Python+Dash快速web应用开发——交互表格篇(中)

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...

  3. 程序员带你十天快速入门Python,玩转电脑软件开发(一)

    关注今日头条-做全栈攻城狮,学代码也要读书,爱全栈,更爱生活.提供程序员技术及生活指导干货. 如果你真想学习,请评论学过的每篇文章,记录学习的痕迹. 请把所有教程文章中所提及的代码,最少敲写三遍,达到 ...

  4. (数据科学学习手札102)Python+Dash快速web应用开发——基础概念篇

    本文示例代码与数据已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的新系列教程Python+Dash快 ...

  5. (数据科学学习手札103)Python+Dash快速web应用开发——页面布局篇

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...

  6. 学习版pytest内核测试平台开发万字长文入门篇

    前言 2021年,测试平台如雨后春笋般冒了出来,我就是其中一员,写了一款pytest内核测试平台,在公司落地.分享出来后,有同学觉得挺不错,希望能开源,本着"公司代码不要传到网上去,以免引起 ...

  7. [Python] 利用Django进行Web开发系列(二)

    1 编写第一个静态页面——Hello world页面 在上一篇博客<[Python] 利用Django进行Web开发系列(一)>中,我们创建了自己的目录mysite. Step1:创建视图 ...

  8. APP敏捷测试,测试和开发并行!

    测试和开发具有同等重要的作用,从一开始,测试和开发就是相向而行的.测试是开发团队的一支独立的.重要的支柱力量. 测试要具备独立性,独立分析业务需求,独立配置测试环境,独立编写测试脚本,独立开发测试工具 ...

  9. Python:渗透测试开源项目

    Python:渗透测试开源项目[源码值得精读] sql注入工具:sqlmap DNS安全监测:DNSRecon 暴力破解测试工具:patator XSS漏洞利用工具:XSSer Web服务器压力测试工 ...

随机推荐

  1. Java 注解 So Easy!!!

    Java注解 Annotations, a form of metadata, provide data about a program that is not part of the program ...

  2. LVS的部署、案例、以及常见问题

    LVS的部署.案例.以及常见问题 原创chenhuyang 最后发布于2018-06-03 16:18:25 阅读数 1560 收藏 展开 一.LVS的部署 LVS现在已经集成在linux内核模块中, ...

  3. 前端经典面试题解密:JS的new关键字都干了什么?

    前言 new关键字在实例化获取对象时都做了什么?是一道经常出现在前端面试时的问题.如果只是简单的了解new关键字是实例化构造函数获取对象,是万万不能够的.更深入的层级发生了什么呢?同时面试官想从这道题 ...

  4. 玩转控件:封装Dev的SearchLookupEdit

    鸣谢 随着前面几个章节对控件封装与扩展的分享,不少小伙伴儿们在作者公众号上反馈,并联系作者,表示通过这些系列和源码能学到不少细节上的东西,并运用到了自己的实际项目当中,也有不少伙伴儿反馈更好更优的处理 ...

  5. 数据库学习 day1 认识数据库

    从SQL的角度而言,数据库是一个以某种有组织的方式储存的数据集合. 我们可以把它比作一个“文件柜”,这个“文件柜”是一个存放数据的物理位置,不管数据是什么,也不管数据是如何组织的. 下面介绍几个术语 ...

  6. php数据库应用程序建议

    一.保持独立的读写连接 开始就创建两个数据库连接是一个好的方法,一个用于读取,一个用于写入,并且允许不同的数据库服务器连接他们.如果只有一个服务器,则将它们设置彼此相同. 当操作为INSERT, UP ...

  7. readthedocs网托管持多语言文档

    希望在readthedocs上创建支持多语言的文档,效果类似: 通过语言选项,可以切到到不同的语言版本:实现这个目标包含两个主要步骤: 在本地对文档进行翻译 在readthedocs.org上配置翻译 ...

  8. Hadoop(九):Shuffle组件

    重温MR整体流程 工作流程 开始执行输入(InputFormat),先对文件进行分片,然后读取数据输入到Map中. Mapper读取输入内容,解析成键值对,1行内容解析成1个键值对,每个键值对调用一次 ...

  9. pip 命令参数以及如何配置国内镜像源

    文章更新于:2020-04-05 注:如果 pip 命令不可以用,参见:python pip命令不能用 文章目录 一.参数详解 1.命令列表 2.通用参数列表 二.实际应用 1.常用命令 2.`pip ...

  10. django发邮件

    django发邮件 配置setting信息 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'sm ...