- Preface 前言
- 第1章 引论
- 第2章 编程惯用法
- 第3章 基础语法
- 建议19:有节制地使用 from…import 语句
- 建议20:优先使用 absolute import 来导入模块
- 建议21:i+=1 不等于 ++i
- 建议22:使用 with 自动关闭资源
- 建议23:使用 else 子句简化循环(异常处理)
- 建议24:遵循异常处理的几点基本原则
- 建议25:避免 finally 中可能发生的陷阱
- 建议26:深入理解 None 正确判断对象是否为空
- 建议27:连接字符串应优先使用 join 而不是 +
- 建议28:格式化字符串时尽量使用 .format 方式而不是 %
- 建议29:区别对待可变对象和不可变对象
- 建议30:[]、() 和 {}:一致的容器初始化形式
- 建议31:记住函数传参既不是传值也不是传引用
- 建议32:警惕默认参数潜在的问题
- 建议33:慎用变长参数
- 建议34:深入理解 str() 和 repr() 的区别
- 建议35:分清 staticmethod 和 classmethod 的适用场景
- 第4章 库
- 建议36:掌握字符串的基本用法
- 建议37:按需选择 sort() 或者 sorted()
- 建议38:使用 copy 模块深拷贝对象
- 建议39:使用 Counter 进行计数统计
- 建议40:深入掌握 ConfigParser
- 建议41:使用 argparse 处理命令行参数
- 建议42:使用 pandas 处理大型 CSV 文件
- 建议43:一般情况使用 ElementTree 解析 XML
- 建议44:理解模块 pickle 优劣
- 建议45:序列化的另一个不错的选择 JSON
- 建议46:使用 traceback 获取栈信息
- 建议47:使用 logging 记录日志信息
- 建议48:使用 threading 模块编写多线程程序
- 建议49:使用 Queue 使多线程编程更安全
- 第5章 设计模式
- 第6章 内部机制
- 建议54:理解 built-in objects
- 建议55:init() 不是构造方法
- 建议56:理解名字查找机制
- 建议57:为什么需要 self 参数
- 建议58:理解 MRO 与多继承
- 建议59:理解描述符机制
- 建议60:区别 getattr() 和 getattribute() 方法
- 建议61:使用更为安全的 property
- 建议62:掌握 metaclass
- 建议63:熟悉 Python 对象协议
- 建议64:利用操作符重载实现中缀语法
- 建议65:熟悉 Python 的迭代器协议
- 建议66:熟悉 Python 的生成器
- 建议67:基于生成器的协程及 greenlet
- 建议68:理解 GIL 的局限性
- 建议69:对象的管理与垃圾回收
- 第7章 使用工具辅助项目开发
- 第8章 性能剖析与优化
建议74:为包编写单元测试
当我们创建了一个包,接着就开始为它编写业务逻辑的代码。比如在前文中,我们创建了arithmetic包,并在里面增加一个加法函数,如下:
def add(x, y): return x + y
无名氏说:“当你写下代码,bug随之而来”。所以我们需要对代码进行测试,以便交付物在交付给业务的下游部门使用时有一定质量保障。对于一个函数而言,最简单的方法也许就是为它编写一些单元测试代码了。
if __name__ == "__main__": assert add(1, 2) == 3 assert add(1, -1) == 0
这样,当以arithmetic.py为入口文件执行arithmetic.py的时候,就会运行这些测试代码,实现对add()函数的质量检测。像这种针对函数编写的测试,我们称为“单元测试”,它是白盒测试的一种,所以单元测试用例都是根据函数的代码而制定的。通过单元测试,可以有效地避免软件退化,增进软件质量,并更快地产生健壮的代码。甚至对开发人员来说,单元测试用例也是最好的文档。
虽然上例让大家感觉测试非常简单,但实际项目中的测试也有不少麻烦:
1)程序员希望测试更加自动化,想象一下,如果加减乘除4个函数不是实现在arithemtic.py一个文件中,而是分列在4个文件中,那么要测试它们就需要分别运行这4个文件。再想象一下,实际项目中可能一个包中有几十甚至上百个文件,那么想要全部测试一次就非常困难。
2)一个测试用例往往在测试之前需要进行打桩或做一些准备工作,在测试之后要清理现场,最好有一个框架可以自动完成这些工作。
3)对于大项目,大量的测试用例需要分门别类地放置,而测试之后,分别产生相应的测试报告。
Python是一门务实的语言,所以自带的电池中就包含了一个名为unittest的模块,可以解决这些问题。关于unittest的知识,我们在建议73中已经学过,接下来就看一下如何使用unittest进行测试的代码。
import unittest import arithmetic class TestCase(unittest.TestCase): def test_add(self): self.assertEqual(arithmetic.add(1, 1), 2) if __name__ == "__main__": unittest.main() 把这些代码保存到 test_arithmetic.py 中,然后执行命令: >python test_arithmetic.py . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
虽然没有显式地调用TestCase.test_add,但从Ran1 test in 0.000s这句输出中可以看到这个测试用例已经执行到了,这就是框架的好处。除了自动调用匹配以test开头的方法之外,unittest.TestCase还有模板方法setUp()和tearDown(),通过覆盖这两个方法,能够实现在测试之前执行一些准备工作,并在测试之后清理现场。
然后再回到最初的假设:在arithmetic项目中,若加减乘除4个函数分别在不同的文件中,那么测试用例也可能分别写在4个文件中,那么运行python test_xxx.py命令的形式就无法简化测试工作。这时候可以使用unittest的测试发现(test discover)功能。
>python -m unittest discover . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
unittest将递归地查找当前目录下匹配test*.py模式的文件,并将其中unittest.TestCase的所有子类都实例化,然后调用相应的测试方法进行测试,这就一举解决了“通过一条命令运行全部测试用例”的问题。
unittest的测试发现功能是Python 2.7版本中才有的,如果你在使用更旧的版本,请安装unittest2。
尽管unittest的测试发现功能已经非常方便,但是因为它需要高版本的Python支持,所以大家喜欢使用setuptools的扩展命令test。
>python setup.py test running test running egg_info writing arithmetic.egg-info/PKG-INFO writing top-level names to arithmetic.egg-info/top_level.txt writing dependency_links to arithmetic.egg-info/dependency_links.txt writing entry points to arithmetic.egg-info/entry_points.txt reading manifest file 'arithmetic.egg-info/SOURCES.txt' writing manifest file 'arithmetic.egg-info/SOURCES.txt' running build_ext test_add (test_arithmetic.TestCase) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
setuptools对distutils.commands进行了扩展,增加了test命令。如上例所示,这个命令执行的时候,先运行egg_info和build_ext子命令构建项目,然后把项目路径加到sys.path中,再搜寻所有的测试套件(test suite,通常指多个测试用例或测试套件的组合),并运行之。要使用这个扩展命令,需要在调用setup()函数的时候向它传递test_suite元数据。比如在arithmetic项目中,是这样的:
>cat setup.py ... setup(name='arithmetic', ... test_suite = "test_arithmetic", ...
test_suite元数据的值可以指向一个包、模块、类或函数,比如在著名的flask项目中,是test_suite='flask.testsuite.suite',其中flask.testsuite.suite是一个函数;而在arithmetic项目中,test_arithmetic是一个模块。
使用setuptools的测试发现功能,可以给开发人员更一致的开发体验,就像使用build、install命令一样,所以受到了大家的喜爱。但是来自unittest本身的缺陷让开发人员想要找到一个更好的测试框架。
1)unittest并不够Pythonic,比如从JUnit中继承而来的首字母小写的骆驼命令法;所有的测试用例都需要从TestCase继承。
2)unittest的setUp()和tearDown()只是在TestCase的层面上提供,即每一个测试用例执行的时候都会运行一遍,如果有许多模块需要测试,那么创建环境和清理现场操作都会带来大量工作。
3)unittest没有插件机制进行功能扩展,比如想要增加测试覆盖统计特性就非常困难。
nose就是作为更好的测试框架进入大家视线的,而且它更是一个具有更强大的测试发现运行的程序。此外nose定义了插件机制,使得扩展nose的功能成为可能(默认自带coverage插件)。使用pip、easy_install安装以后,就多了一个nosetests命令可以使用。比如在arithmetic项目中运行:
>nosetests -v test_add (test_arithmetic.TestCase) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.004s OK
可以看到nose能够自动发现测试用例,并调用执行,由于它与原有的unittest测试用例兼容,所以可以随时将它引入到项目中来。其实nose的测试发现机制更进一步,它抛弃了unittest中测试用例必须放在TestCase子类中的限制,只要命名符合(?:^|[b_.-])[Tt]est正则表达式的类和函数都可作为测试用例运行。
此外,nose作为一个测试框架,也提供了与unittest.TestCase类似的断言函数,但它抛弃了unittest的那种Java风格的命令方式,使用的是符合PEP8的命名方式。
针对unittest中setUp()和tearDown()只能放在TestCase中的问题,nose提供了3个级别的解决方案,这些配置和清理函数,可以放在包(__init__.py文件中)、模块和测试用例中,非常完美地解决了不同层次的测试需要的配置和清理需求。
最后,nose与setuptools的集成更加友好,提供了nose.collector作为通过的测试套件,让开发人员无须针对不同项目编写不同的套件。比如针对arithmetic项目的setup.py文件作如下修改:
>cat setup.py ... setup(name='arithmetic', ... # test_suite = "test_arithmetic", test_suite = "nose.collector", ...
然后运行python setup.py test,得到的结果是一样的。因为使用了nose.collector之后,test_suite元数据就确定不变了,所以它也非常适合写入paster的模板中去,在构建目录的时候自动生成。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论