简介
单元测试就是编写一些测试代码,用来对一个函数,类对象,或者模块进行检测。一来可以保证程序执行的正确性,二来也可以尽量避免低级别的bug以及改动引发的其他模块bug(比如修1个bug,多出100个bug这种)。并且还有大名鼎鼎的TDD(Test-Driven Development)测试驱动开发。所以用测试来保证我们代码符合预期需求是非常重要的,也能让我们对产品的迭代更加有信心巴拉巴拉…是吧,反正很重要就是了。但是很多程序员(包括我)都是不怎么喜欢写测试的,毕竟是测试而不是功能代码,对自己的功(la)能(ji)代码自信得一批。幸好是刚出来的应届生还可以慢慢适应,不然不写单测怕是没人要了。
单测工具(unittest)
单测框架工具有很多,像Java就有Java的Junit,unittest就是python中的Junit。这个框架是Python自带的,其官方文档地址如下:
https://docs.python.org/3.6/library/unittest.html
文档是英文的,不过基本的还是能看懂的,虽然我目前还没去啃,毕竟英语也是半斤八两,别看了半天手头上的工作还没完成。先找找一些中文的博客看看,顺便借(cao)鉴(xi)一下他们漂亮的图,是吧。
对于unittest主要有6个概念要搞清:
- Test Loader:用来加载测试案例,即扫描以test开头的Test case方法,加载到run中运行。
- Test Case:一个TestCase即为一个测试用例,包括了测试用例函数执行前的准备工作setUp,执行代码run,以及执行完之后的tearDown还原工作。
- Test Suite:多个测试用例的集合,就是将多个Test Case组合起来,共同执行的一套集合,可以用addTest或者addTests进行测试用例的添加。
- Test Runner:执行测试用例,既可以是Test Case,也可以是Test Suite。执行之后将结果送入Test Result中。
- Test Result:管理Test Runner执行后的用例结果,是正确还是错误还是跳过,执行状态是:F(失败),.(成功),E(执行出错),S(跳过执行)
- Test Fixture:测试用例的环境准备。
基本方法
编写测试用例class的方法
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
断言
我们测试一个过程正确或者错误都是通过断言。
方法 | 检测例子 | 首次出现的Python版本 |
---|---|---|
assertEqual(a, b) | a == b | |
assertNotEqual(a, b) | a != b | |
assertTrue(x) | bool(x) is True | |
assertFalse(x) | bool(x) is False | |
assertIs(a, b) | a is b | 3.1 |
assertIsNot(a, b) | a is not b | 3.1 |
assertIsNone(x) | x is None | 3.1 |
assertIsNotNone(x) | x is not None | 3.1 |
assertIn(a, b) | a in b | 3.1 |
assertNotIn(a, b) | a not in b | 3.1 |
assertIsInstance(a, b) | isinstance(a, b) | 3.2 |
assertNotIsInstance(a, b) | not isinstance(a, b) | 3.2 |
assertEqual(a, b) | a == b | |
assertNotEqual(a, b) | a != b |
跳过执行装饰器
-
@unittest.skip(reason) 直接跳过,不执行
-
@unittest.skipIf(condition, reason) condition为真时跳过
-
@unittest.skipUnless(condition, reason) condition为假时跳过
-
@unittest.expectedFailure 测试执行失败时跳过统计
代码练习
偷偷看看官方文档,然后借鉴几个例子。
- 初步尝试 官方的第一个例子,简单感受一下测试的魅力。这里主要是测试了字符串大小写以及异常分割的断言。
1 |
|
看下输出结果:
1 |
|
这里3个案例都执行成功了,我们来搞两个错的试试。
1 |
|
输出结果如下:
1 |
|
这里我们制造了两个错误,一个是函数功能测试错误,另一个是抛出异常错误,注意E和F的区别。F是指你的测试案例执行失败(案例执行成功),E是指你的测试案例中存在执行错误(一般是测试代码写错)。现在看到测试结果出现全对的时候也是有那么点小激动的。
- 方法执行流程 刚刚前面说了那么多方法,那么每个方法的执行流程是怎么样的呢?让我们来尝试一下。
1 |
|
执行结果如下:
1 |
|
- 加载多个 假设我现在有多个testcase,我要一起加载执行怎么办呢。这时候就可以用到我们的load和suite了。
- suite.addTest() # 加载一个测试用例
- suite.addTests() # 加载系列测试用例(按加载过程有序)
单个添加
1 |
|
多个添加
1 |
|
利用Load添加
1 |
|
原理分析
让我们来看看框架的类图(来自网络):
首先,由TestLoader进行扫描加载,把所有test开头的TestCase测试案例都加入到TestSuite,再由TestRunner运行suite,最后将执行结果输出到result。对于更详细的执行原理,更加详细的执行过程,可以通过runner.py文件源码进行查看。
覆盖率(coverage)
简介
Coverage是一种用于统计Python代码覆盖率的工具,通过它可以检测测试代码对被测代码的覆盖率如何。Coverage支持分支覆盖率统计,可以生成HTML/XML报告。官方文档地址:
https://coverage.readthedocs.io/en/latest/
用法
- 控制台显示覆盖率:
1 |
|
- 生成HTML网页报告(用HTMLTestRunner也可以生成报告,不过对于py3的支持要修改部分代码)
1 |
|
原理分析
分支检测原理:在这里,Coverage利用了trace追踪机制,你肯定接触过这东西,就是调试的时候。而对于web程序就更厉害了,web程序一般是循环监听,除非异常情况否则不会自动退出。而Coverage在实现上使用了atexit模块注册一个回调函数,在Python退出时将内存中的覆盖率结果写到文件中。被测脚本只有正常退出或者以SIGINT2信号退出才能出发atexit,才能得到覆盖率结果。CTRL+C发的即是SIGINT2信号,所以前台启动的服务用CTRL+C停止后可以出结果。而atexit它内部又是通过sys.exitfunc来实现的。
模拟数据(mock)
简介
我们每次跑测试,肯定需要一些资源数据,但是不可能每次都用真实数据去跑。例如,测试资源不可用,或者不适合,他正开发中,测试资源太昂贵,不可预知,等等情况。这时候我们就需要使用mock来进行数据的模拟。官方文档地址:
https://docs.python.org/3.6/library/unittest.mock.html
注意Python3中mock是自带的,python2中还要自己安装,pip install mock 一下即可,这里我使用的是python3
基本方法
这里我们主要介绍一下,mock时的传参:
1 |
|
- name:这个是用来命名一个mock对象,只是起到标识作用,打印mock对象name的时候可以看到。
- spec: 可以是字符串列表,也可以是充当模拟对象规范的现有对象(类或实例)。如果传入一个对象,则通过在对象上调用dir来形成字符串列表(不包括不受支持的魔术属性和方法)。访问不在此列表中的任何属性将引发AttributeError。如果spec是一个对象(而不是一个字符串列表),那么__class__将返回spec对象的类,允许模拟通过isinstance()测试。
- spec_set: 比sepc更严格,用sepc你还可以设置一个未指定的属性。 使用spec_set后尝试在mock上设置或获取不在spec_set传递的对象上的属性将引发AttributeError。
- side_effect: 例如:
mock.Mock(return_value=10, side_effect=code.my_mvalue)
设置此属性之后mock的return_value不生效,调用原my_value真正的返回值 - return_value: 例如:
mock.Mock(return_value=10)
设置之后,调用时将返回设定的值,原函数的返回值将无效 - unsafe: 3.5版本新特性。默认情况下,如果任何属性以assert或assret开头,则会引发AttributeError。 传递unsafe = True将允许访问这些属性。
代码练习
- 常规操作 让我们来mock一个价值百万的数据返回
1 |
|
看下输出,发现一个是成功的,一个是失败的,说明我们的mock数据是生效了的:
1 |
|
- 使用装饰器mock一个模块下的类
这里我们先mock一下Code类,然后测试Use类,由于Use类调用了Code,因此可以达到我们想要的mock结果。
注意:
- 参数中的mock_Code不是像test那样限制开头,可以随便命名
- 这里我把类都写在了一个文件里面,所以使用了’main‘(当前运行文件会设为这个值,下回有机会讨论一下)
1 |
|
输出
1 |
|
原理分析
让我们看下Mock的类继承关系:
Mock继承自CallableMixin, NonCallableMock,而CallableMixin, NonCallableMock又是继承自Base,Base里面的代码其实只有4行,定义了return_value和side_effect的默认值,而具体的一些方法则是写入了CallableMixin和NonCallableMock:
1 |
|
CallableMixin中主要是负责调用逻辑封装,签名检查,对象调用统计等工作以及最重要的我们要使用的一些调用,而NonCallableMock中则是重写一些魔法方法来达到mock数据的目的,魔法方法的一些用法目前还不是很懂就不献丑了。
集成框架(nose)
nose框架主要是把一些测试的流程都给串了起来,包括自动发现test命名的测试案例进行测试,覆盖率,报告生成之类的,在大型项目上可以应用上。这个不是python的标准库,需要使用pip install nose
来进行安装。安装之后使用nosetests -h
可以查看相关的命令。
总结
怎么说,这一套流程搞下来其实感觉花的时间并不会比开发过程少,甚至可能开发一小时,测试两小时都有可能。不过为了保证代码能够如期执行,多人协作的时候不容易出岔子,还是搞一搞比较好。毕竟写测试的时候需要你往很多方面去思考这个功能的执行状态,能够尽可能地避免bug(大神请绕道)。希望以后的工作中能养成写优秀测试的习惯吧。