详解Python的单元测试


Posted in Python onApril 28, 2015

如果你听说过“测试驱动开发”(TDD:Test-Driven Development),单元测试就不陌生。

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

比如对函数abs(),我们可以编写出以下几个测试用例:

  •     输入正数,比如1、1.2、0.99,期待返回值与输入相同;
  •     输入负数,比如-1、-1.2、-0.99,期待返回值与输入相反;
  •     输入0,期待返回0;
  •     输入非数值类型,比如None、[]、{},期待抛出TypeError。

把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。

如果单元测试通过,说明我们测试的这个函数能够正常工作。如果单元测试不通过,要么函数有bug,要么测试条件输入不正确,总之,需要修复使单元测试能够通过。

单元测试通过后有什么意义呢?如果我们对abs()函数代码做了修改,只需要再跑一遍单元测试,如果通过,说明我们的修改不会对abs()函数原有的行为造成影响,如果测试不通过,说明我们的修改与原有行为不一致,要么修改代码,要么修改测试。

这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。

我们来编写一个Dict类,这个类的行为和dict一致,但是可以通过属性来访问,用起来就像下面这样:

>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1

mydict.py代码如下:

class Dict(dict):

  def __init__(self, **kw):
    super(Dict, self).__init__(**kw)

  def __getattr__(self, key):
    try:
      return self[key]
    except KeyError:
      raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

  def __setattr__(self, key, value):
    self[key] = value

为了编写单元测试,我们需要引入Python自带的unittest模块,编写mydict_test.py如下:

import unittest

from mydict import Dict

class TestDict(unittest.TestCase):

  def test_init(self):
    d = Dict(a=1, b='test')
    self.assertEquals(d.a, 1)
    self.assertEquals(d.b, 'test')
    self.assertTrue(isinstance(d, dict))

  def test_key(self):
    d = Dict()
    d['key'] = 'value'
    self.assertEquals(d.key, 'value')

  def test_attr(self):
    d = Dict()
    d.key = 'value'
    self.assertTrue('key' in d)
    self.assertEquals(d['key'], 'value')

  def test_keyerror(self):
    d = Dict()
    with self.assertRaises(KeyError):
      value = d['empty']

  def test_attrerror(self):
    d = Dict()
    with self.assertRaises(AttributeError):
      value = d.empty

编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。

以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。

对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEquals():

self.assertEquals(abs(-1), 1) # 断言函数返回的结果与1相等

另一种重要的断言就是期待抛出指定类型的Error,比如通过d['empty']访问不存在的key时,断言会抛出KeyError:

with self.assertRaises(KeyError):
  value = d['empty']

而通过d.empty访问不存在的key时,我们期待抛出AttributeError:

with self.assertRaises(AttributeError):
  value = d.empty

运行单元测试

一旦编写好单元测试,我们就可以运行单元测试。最简单的运行方式是在mydict_test.py的最后加上两行代码:

if __name__ == '__main__':
  unittest.main()

这样就可以把mydict_test.py当做正常的python脚本运行:

$ python mydict_test.py

另一种更常见的方法是在命令行通过参数-m unittest直接运行单元测试:

$ python -m unittest mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

这是推荐的做法,因为这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试。
setUp与tearDown

可以在单元测试中编写两个特殊的setUp()和tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。

setUp()和tearDown()方法有什么用呢?设想你的测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:

class TestDict(unittest.TestCase):

  def setUp(self):
    print 'setUp...'

  def tearDown(self):
    print 'tearDown...'

可以再次运行测试看看每个测试方法调用前后是否会打印出setUp...和tearDown...。
小结

单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。

单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。

单元测试代码要非常简单,如果测试代码太复杂,那么测试代码本身就可能有bug。

单元测试通过了并不意味着程序就没有bug了,但是不通过程序肯定有bug。

Python 相关文章推荐
python中模块查找的原理与方法详解
Aug 11 Python
python实现简单聊天应用 python群聊和点对点均实现
Sep 14 Python
用python 批量更改图像尺寸到统一大小的方法
Mar 31 Python
python使用scrapy发送post请求的坑
Sep 04 Python
Python使用Tkinter实现滚动抽奖器效果
Jan 06 Python
Django接收照片储存文件的实例代码
Mar 07 Python
用python打开摄像头并把图像传回qq邮箱(Pyinstaller打包)
May 17 Python
Python基于xlutils修改表格内容过程解析
Jul 28 Python
Python同时迭代多个序列的方法
Jul 28 Python
安装pyecharts1.8.0版本后导入pyecharts模块绘图时报错: “所有图表类型将在 v1.9.0 版本开始强制使用 ChartItem 进行数据项配置 ”的解决方法
Aug 18 Python
解决pytorch下出现multi-target not supported at的一种可能原因
Feb 06 Python
Python如何配置环境变量详解
May 18 Python
Python xlrd读取excel日期类型的2种方法
Apr 28 #Python
Python发送email的3种方法
Apr 28 #Python
Python中使用partial改变方法默认参数实例
Apr 28 #Python
调试Python程序代码的几种方法总结
Apr 28 #Python
解析Python中的异常处理
Apr 28 #Python
python调用java模块SmartXLS和jpype修改excel文件的方法
Apr 28 #Python
Python EOL while scanning string literal问题解决方法
Sep 18 #Python
You might like
将博客园(cnblogs.com)数据导入到wordpress的代码
2013/01/06 PHP
PHP经典面试题集锦
2015/03/19 PHP
详解PHP的Yii框架中自带的前端资源包的使用
2016/03/31 PHP
BOOM vs RR BO5 第二场 2.14
2021/03/10 DOTA
FireFox JavaScript全局Event对象
2009/06/14 Javascript
JavaScript 密码强度判断代码
2009/09/05 Javascript
JQuery最佳实践之精妙的自定义事件
2010/08/11 Javascript
JavaScript打印iframe内容示例代码
2013/08/20 Javascript
代码获取历史上的今天发生的事
2014/04/11 Javascript
NodeJS Express框架中处理404页面一个方式
2014/05/28 NodeJs
在浏览器中打开或关闭JavaScript的方法
2015/06/03 Javascript
超级给力的JavaScript的React框架入门教程
2015/07/02 Javascript
jQuery获得字体颜色16位码的方法
2016/02/20 Javascript
辨析JavaScript中的Undefined类型与null类型
2016/05/26 Javascript
js HTML5多图片上传及预览实例解析(不含前端的文件分割)
2016/08/26 Javascript
详解js跨域请求的两种方式,支持post请求
2018/05/05 Javascript
JS实现可视化文件上传
2018/09/08 Javascript
对angularJs中2种自定义服务的实例讲解
2018/09/30 Javascript
浅谈Vue数据响应
2018/11/05 Javascript
Python3实现发送QQ邮件功能(文本)
2017/12/15 Python
Python wxPython库使用wx.ListBox创建列表框示例
2018/09/03 Python
Python3将ipa包中的文件按大小排序
2020/04/17 Python
html5中JavaScript removeChild 删除所有节点
2014/05/16 HTML / CSS
英国旅游额外服务市场领导者:Holiday Extras(机场停车场、酒店、接送等)
2017/10/07 全球购物
台湾网友喜爱的综合型网路购物商城:Yahoo! 奇摩购物中心
2018/03/10 全球购物
南非最大的在线时尚商店:Zando
2019/07/21 全球购物
MIKI HOUSE美国官方网上商店:日本领先的婴儿和儿童高级时装品牌
2020/06/21 全球购物
成功的餐厅经营创业计划书
2014/01/15 职场文书
品质主管岗位职责
2014/03/16 职场文书
计算机考试作弊检讨书1000字
2015/01/01 职场文书
学术会议邀请函
2015/01/30 职场文书
2015年七一建党节活动方案
2015/05/05 职场文书
离职信范本
2015/06/23 职场文书
Windows Server 2012配置DNS服务器的方法
2022/04/29 Servers
js面向对象编程OOP及函数式编程FP区别
2022/07/07 Javascript
win7配置本地ftp服务器的图文教程
2022/08/05 Servers