从unittest说起
0. 谈谈 ‘编写测试’
要是有 《程序员最讨厌做的事》 排行榜,top 10中一定会出现写注释和读别人的注释这两个事,不出意外的话 编写测试 也会存在其中。编写测试对开发有什么意义么? 我身边的朋友同事对这个问题大致有三种答案
- 没有意义,太浪费时间了!需求还写不完写啥测试!
- 有意义!它能……. balabala 一堆好处
- 你需要测试?去找tester呀!和我developer有啥关系! (鲁迅表示很赞!)
而我个人对编写测试这件事的看法是,开发者需要去进行测试的编写,只写了代码而没有完整的测试,即使能运行起来你也不敢保证它没有问题。我觉得完整测试对项目的好处:
- 功能更加的有保障,测试的意义就在于覆盖各种情况,来试探功能是否能按照预期来执行。你的功能测试case越多,你对这个feature的信息就会越大,线上出现bug的概率也会越小。
- 团队沟通成本变低,当需要一个功能时,可能有很多种实现它的方法,而每个人会根据个人喜好等等因素去选择其中一种来做。那怎么说明你的方法就是有效的呢?跑过测试就好了。
- 修改代码时功能更保障,开发无法绕过的事就是修改代码,那修改后的功能和之前有什么不同么?谁也无法保证,但测试case能保证,case不会也不应该随着你代码变化而变化,除非你就是要将原有功能改为另外一种样式,否则case不变的情况下,它所预期的功能若有变化,那就是改错了。
然而,若做的项目是一个最多维护几个月甚至更短时间的玩具项目,那没有测试也是可以接受的。一个项目要做到有完整的测试,是需要花费很多时间和精力的,所以,到底要不要在项目里构建完整的测试,需要根据实际情况来谈。
1. 开始聊 unittest
test也是一门很大的学问,并不是一两句能说清楚的,我也只是一个初学者,只能分享出我认为理解还比较深刻的方向。
这里先只谈论unittest,单元测试顾名思义就是针对程序的各个模块的最小单元(函数)进行逻辑和功能的测试。
而我认为单元测试应侧重于校验逻辑,不该去过度关心功能的校验,功能的校验可以放心的交由针对模块整体功能的集成测试来进行,这样更加的“纯粹”,以下也是这么做的一些理由:
- 逻辑测试都不能通过,功能一定不正确,功能只是逻辑的集合。
- 模块最小单元(函数) 的功能纯粹,逻辑测试基本等同于功能测试了。
- 功能测试往往会有很多的依赖(例如网络等),而逻辑测试可以屏蔽掉它们,让单元测试能更简单的运行起来,提高开发的效率。
个人理解,好的单元测试应该是完全脱离外部依赖的。什么意思呢?以python为例,被测试的模块中引用了其他模块(如 第三方包requests 或python 内部包 os 等等), 测试环境中也不应该为它们做任何额外的准备。
换句话说,你的unittest 应该能在只安装python,没有任何其他package的情况下也能运行,这样才做到了对外部依赖的完全脱离。
2. 如何写unittest
上面提到的条件看似很苛刻,其实是很好实现的,如下面示例,在package文件夹下有demo文件 和它的test file, 只要运行 python -m unittest package/demo_test.py 就能发现测试能完美的成功,并不需要去安装any_package(事实上也不存在这个package)。
1 |
|
1 |
|
1 |
|
测试中用到了 mock 这个package, 它在Python3中是默认的,在2里需要install (PS: 它并不是能算外部依赖)。
根据以上的示例,我们是完全可以在没有外部依赖的情况下对某个模块的逻辑进行测试的。
这里我不想去过多的解释怎么用mock,有兴趣的可以去看下官方mock使用示例。而为什么这么写就可以了,我也不打算画很大的篇幅去介绍,很多都是python的一些原理特性,并没有什么好重点说的。
我想花费更大笔墨的部分是关于case本身的一些规范,我觉得这些规范更加的重要,因为写case并不仅仅是为了验证功能,还可以作为一个高效沟通的方式,就像上面有提到的,如何证明功能有保证,靠的是有实际case passed,若case写的晦涩难懂其实还是很难说服他人你的功能没有问题的。
先大概解释下上面的case发生了什么。
首先我们做了with mock.patch.dict(sys.modules, {"any_package": mock_any_package}), 它的作用就是将 sys.modules 中 “any_package” 对应的对象替换为mock_any_package,若sys.modules压根没有 “any_package”,那就新加上它。 而 sys.modules是做什么的呢?它就是保存当前运行时中package name和 package object的一个变量。
通俗的解释,当你import package 时,python 会去 sys.modules 中寻找对应的 package, 这么解释应该就明白上面case中引用Democlass时,源文件中 from any_package import AnyClass为什么不报错了吧。
之后的事情就没什么好说的了,由于将压根不存在的”any_package” 填充上了一个准备好的mock对象,所有的行为也都会按照预期进行了
case 规范
- case命名规范
好的命名可以让人一下看出来这个case到底是关于什么的,相反,会让人疑惑你到底在测试什么?
个人推荐的命名规范如下
test_{函数名}_{调用函数使用的值(以with开头)} _{调用函数前的准备(以when开头)}
示例:
如果要测试 get_pod(servic_name), 使用的service_name是None,在ip pool被更新后。
可以为以下名字
- test_get_pod_with_invalid_service_name_when_ip_pool_updated
- test_get_pod_with_service_name_is_none_when_ip_pool_updated
- case 内书写规范
上面的示例case中就是按照一个规范来写的,我觉得是个很不错的规范。能让人很清楚的知道各部分代码都是干什么的。其实就是那几个注释,每个的意义与注意项如下:
- Expected: 期望的结果
- 解释: 它是一个预期值,即当我们调用完被测试函数后,我们预计它会怎么样,一般都是预期一个值或几个值,与调用完成后的一些值做assert
- 注意: 所有的case应该都有Expected项,测试的预期都没有,那怎么算这个测试是有效的呢?
- Given: 准备好的
- 解释: 它包括被测试函数调用前所需的一切条件准备,主要是做一些调用所需值的准备,有时也要做一些复杂的准备,这都取决于被测试测试函数需要什么
- When: 被测试函数被调用时
- 注意: 大多数情况下When中应该只有一行代码,即 被测试函数被调用那行代码。
- Then: 测试结果校验
- 解释: 这里就是做预期结果和实际结果的比对,看看是否被测试函数在准备好的条件下,功能的逻辑是否都是预期的的样子
写这篇文章前后跨度得有一个月,期间各种琐事干扰,中途有好些天连电脑都来不及打开更别提写文章了。自己在写的过程中也有很大的困惑,没写之前觉得有很多干货要分享,等到真正写到中间段落,觉得自己分享的都只是一家之言,总会给人一种刻板偏见的印象,中途有很多次都想放弃写它了。
但最终还是硬着头皮更新完了。我内心还是觉得上面的分享有很多是有意义的,例如通过case降低沟通成本,这是我在工作中的真实体会。我也与身边朋友聊过相关的话题,因为项目中没有unittest, 而被各种很低级的错误折磨的经历他们能给我举出无数,而为什么没有无非是项目主管不推动或即使推动大家都没有积极性。我只想通过我的分享让更多人能对unittest感兴趣。虽然它只是解决(避免或减少)编程问题的方法论之一,可能它也不是最有效的,不过在你没有其他的方法论之前,你可以尝试先用它,毕竟有就比没有强。
之后应该会专门针对python 的unittest 技巧(主要是使用mock的一些技巧)再写一篇文章,在这之前我还要再深入研究python unittest相关的模块。
Created At 21 May 13th - 12:00:59am