30 真的有必要写单元测试吗? 你好,我是景霄。

说到unit test(即单元测试,下文统一用中文称呼),大部分人的反应估计有这么两种:要么就是,单元测试啊,挺简单的呀,做不做无所谓吧;要么就是,哎呀,项目进度太赶,单元测试拖一拖之后再来吧。

显然,这两种人,都没有正确认识到单元测试的价值,也没能掌握正确的单元测试方法。你是不是觉得自己只要了解Python的各个feature,能够编写出符合规定功能的程序就可以了呢?

其实不然,完成产品的功能需求只是很基础的一部分,如何保证所写代码的稳定、高效、无误,才是我们工作的关键。而学会合理地使用单元测试,正是帮助你实现这一目标的重要路径。

我们总说,测试驱动开发(TDD)。今天我就以Python为例,教你设计编写Python的单元测试代码,带你熟悉并掌握这一重要技能。

什么是单元测试?

单元测试,通俗易懂地讲,就是编写测试来验证某一个模块的功能正确性,一般会指定输入,验证输出是否符合预期。

实际生产环境中,我们会对每一个模块的所有可能输入值进行测试。这样虽然显得繁琐,增加了额外的工作量,但是能够大大提高代码质量,减小bug发生的可能性,也更方便系统的维护。

说起单元测试,就不得不提 Python unittest库,它提供了我们需要的大多数工具。我们来看下面这个简单的测试,从代码中了解其使用方法: import unittest /# 将要被测试的排序函数 def sort(arr): l = len(arr) for i in range(0, l): for j in range(i + 1, l): if arr[i] >= arr[j]: tmp = arr[i] arr[i] = arr[j] arr[j] = tmp /# 编写子类继承unittest.TestCase class TestSort(unittest.TestCase): /# 以test开头的函数将会被测试 def test_sort(self): arr = [3, 4, 1, 5, 6] sort(arr) /# assert 结果跟我们期待的一样 self.assertEqual(arr, [1, 3, 4, 5, 6]) if name == ‘main’: /#/# 如果在Jupyter下,请用如下方式运行单元测试 unittest.main(argv=[‘first-arg-is-ignored’], exit=False) /#/# 如果是命令行下运行,则: /#/# unittest.main() /#/# 输出 .. ———————————————————————- Ran 2 tests in 0.002s OK

这里,我们创建了一个排序函数的单元测试,来验证排序函数的功能是否正确。代码里我做了非常详细的注释,相信你能够大致读懂,我再来介绍一些细节。

首先,我们需要创建一个类

TestSort ,继承类

‘unittest.TestCase’ ;然后,在这个类中定义相应的测试函数test_sort(),进行测试。注意,测试函数要以

‘test’ 开头,而测试函数的内部,通常使用assertEqual()、assertTrue()、assertFalse()和assertRaise()等assert语句对结果进行验证。

最后运行时,如果你是在IPython或者Jupyter环境下,请使用下面这行代码: unittest.main(argv=[‘first-arg-is-ignored’], exit=False)

而如果你用的是命令行,直接使用unittest.main()就可以了。你可以看到,运行结果输出

’OK‘ ,这就表示我们的测试通过了。

当然,这个例子中的被测函数相对简单一些,所以写起对应的单元测试来也非常自然,并不需要很多单元测试的技巧。但实战中的函数往往还是比较复杂的,遇到复杂问题,高手和新手的最大差别,便是单元测试技巧的使用。

单元测试的几个技巧

接下来,我将会介绍Python单元测试的几个技巧,分别是mock、side_effect和patch。这三者用法不一样,但都是一个核心思想,即用虚假的实现,来替换掉被测试函数的一些依赖项,让我们能把更多的精力放在需要被测试的功能上。

mock

mock是单元测试中最核心重要的一环。mock的意思,便是通过一个虚假对象,来代替被测试函数或模块需要的对象。

举个例子,比如你要测一个后端API逻辑的功能性,但一般后端API都依赖于数据库、文件系统、网络等。这样,你就需要通过mock,来创建一些虚假的数据库层、文件系统层、网络层对象,以便可以简单地对核心后端逻辑单元进行测试。

Python mock则主要使用mock或者MagicMock对象,这里我也举了一个代码示例。这个例子看上去比较简单,但是里面的思想很重要。下面我们一起来看下: import unittest from unittest.mock import MagicMock class A(unittest.TestCase): def m1(self): val = self.m2() self.m3(val) def m2(self): pass def m3(self, val): pass def test_m1(self): a = A() a.m2 = MagicMock(return_value=”custom_val”) a.m3 = MagicMock() a.m1() self.assertTrue(a.m2.called) /#验证m2被call过 a.m3.assert_called_with(“custom_val”) /#验证m3被指定参数call过 if name == ‘main’: unittest.main(argv=[‘first-arg-is-ignored’], exit=False) /#/# 输出 .. ———————————————————————- Ran 2 tests in 0.002s OK

这段代码中,我们定义了一个类的三个方法m1()、m2()、m3()。我们需要对m1()进行单元测试,但是m1()取决于m2()和m3()。如果m2()和m3()的内部比较复杂, 你就不能只是简单地调用m1()函数来进行测试,可能需要解决很多依赖项的问题。

这一听就让人头大了吧?但是,有了mock其实就很好办了。我们可以把m2()替换为一个返回具体数值的value,把m3()替换为另一个mock(空函数)。这样,测试m1()就很容易了,我们可以测试m1()调用m2(),并且用m2()的返回值调用m3()。

可能你会疑惑,这样测试m1()不是基本上毫无意义吗?看起来只是象征性地测了一下逻辑呀?

其实不然,真正工业化的代码,都是很多层模块相互逻辑调用的一个树形结构。单元测试需要测的是某个节点的逻辑功能,mock掉相关的依赖项是非常重要的。这也是为什么会被叫做单元测试unit test,而不是其他的integration test、end to end test这类。

Mock Side Effect

第二个我们来看Mock Side Effect,这个概念很好理解,就是 mock的函数,属性是可以根据不同的输入,返回不同的数值,而不只是一个return_value。

比如下面这个示例,例子很简单,测试的是输入参数是否为负数,输入小于0则输出为1 ,否则输出为2。代码很简短,你一定可以看懂,这便是Mock Side Effect的用法。 from unittest.mock import MagicMock def side_effect(arg): if arg < 0: return 1 else: return 2 mock = MagicMock() mock.side_effect = side_effect mock(-1) 1 mock(1) 2

patch

至于patch,给开发者提供了非常便利的函数mock方法。它可以应用Python的decoration模式或是context manager概念,快速自然地mock所需的函数。它的用法也不难,我们来看代码: from unittest.mock import patch @patch(‘sort’) def test_sort(self, mock_sort): … …

在这个test里面,mock_sort 替代sort函数本身的存在,所以,我们可以像开始提到的mock object一样,设置return_value和side_effect。

另一种patch的常见用法,是mock类的成员函数,这个技巧我们在工作中也经常会用到,比如说一个类的构造函数非常复杂,而测试其中一个成员函数并不依赖所有初始化的object。它的用法如下: with patch.object(A, ‘init’, lambda x: None): …

代码应该也比较好懂。在with语句里面,我们通过patch,将A类的构造函数mock为一个do nothing的函数,这样就可以很方便地避免一些复杂的初始化(initialization)。

其实,综合前面讲的这几点来看,你应该感受到了,单元测试的核心还是mock,mock掉依赖项,测试相应的逻辑或算法的准确性。在我看来,虽然Python unittest库还有很多层出不穷的方法,但只要你能掌握了MagicMock和patch,编写绝大部分工作场景的单元测试就不成问题了。

高质量单元测试的关键

这节课的最后,我想谈一谈高质量的单元测试。我很理解,单元测试这个东西,哪怕是正在使用的人也是“百般讨厌”的,不少人很多时候只是敷衍了事。我也嫌麻烦,但从来不敢松懈,因为在大公司里,如果你写一个很重要的模块功能,不写单元测试是无法通过code review的。

低质量的单元测试,可能真的就是摆设,根本不能帮我们验证代码的正确性,还浪费时间。那么,既然要做单元测试,与其浪费时间糊弄自己,不如追求高质量的单元测试,切实提高代码品质。

那该怎么做呢?结合工作经验,我认为一个高质量的单元测试,应该特别关注下面两点。

Test Coverage

首先我们要关注Test Coverage,它是衡量代码中语句被cover的百分比。可以说,提高代码模块的Test Coverage,基本等同于提高代码的正确性。

为什么呢?

要知道,大多数公司代码库的模块都非常复杂。尽管它们遵从模块化设计的理念,但因为有复杂的业务逻辑在,还是会产生逻辑越来越复杂的模块。所以,编写高质量的单元测试,需要我们cover模块的每条语句,提高Test Coverage。

我们可以用Python的coverage tool 来衡量Test Coverage,并且显示每个模块为被coverage的语句。如果你想了解更多更详细的使用,可以点击这个链接来学习:https://coverage.readthedocs.io/en/v4.5.x/

模块化

高质量单元测试,不仅要求我们提高Test Coverage,尽量让所写的测试能够cover每个模块中的每条语句;还要求我们从测试的角度审视codebase,去思考怎么模块化代码,以便写出高质量的单元测试。

光讲这段话可能有些抽象,我们来看这样的场景。比如,我写了一个下面这个函数,对一个数组进行处理,并返回新的数组: def work(arr): /# pre process … … /# sort l = len(arr) for i in range(0, l): for j in range(i + 1, j): if arr[i] >= arr[j]: tmp = arr[i] arr[i] = arr[j] arr[j] = tmp /# post process … … Return arr

这段代码的大概意思是,先有个预处理,再排序,最后再处理一下然后返回。如果现在要求你,给这个函数写个单元测试,你是不是会一筹莫展呢?

毕竟,这个函数确实有点儿复杂,以至于你都不知道应该是怎样的输入,并要期望怎样的输出。这种代码写单元测试是非常痛苦的,更别谈cover每条语句的要求了。

所以,正确的测试方法,应该是先模块化代码,写成下面的形式: def preprocess(arr): … … return arr def sort(arr): … … return arr def postprocess(arr): … return arr def work(self): arr = preprocess(arr) arr = sort(arr) arr = postprocess(arr) return arr

接着再进行相应的测试,测试三个子函数的功能正确性;然后通过mock子函数,调用work()函数,来验证三个子函数被call过。

from unittest.mock import patch def test_preprocess(self): … def test_sort(self): … def test_postprocess(self): … @patch(‘%s.preprocess’) @patch(‘%s.sort’) @patch(‘%s.postprocess’) def test_work(self,mock_post_process, mock_sort, mock_preprocess): work() self.assertTrue(mock_post_process.called) self.assertTrue(mock_sort.called) self.assertTrue(mock_preprocess.called)

你看,这样一来,通过重构代码就可以使单元测试更加全面、精确,并且让整体架构、函数设计都美观了不少。

总结

回顾下这节课,整体来看,单元测试的理念是先模块化代码设计,然后针对每个作用单元,编写单独的测试去验证其准确性。更好的模块化设计和更多的Test Coverage,是提高代码质量的核心。而单元测试的本质就是通过mock,去除掉不影响测试的依赖项,把重点放在需要测试的代码核心逻辑上。

讲了这么多,还是想告诉你,单元测试是个非常非常重要的技能,在实际工作中是保证代码质量和准确性必不可少的一环。同时,单元测试的设计技能,不只是适用于Python,而是适用于任何语言。所以,单元测试必不可少。

思考题

那么,你在平时的学习工作中,曾经写过单元测试吗?在编写单元测试时,用到过哪些技巧或者遇到过哪些问题吗?欢迎留言与我交流,也欢迎你把这篇文章分享出去。

参考资料

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Python%e6%a0%b8%e5%bf%83%e6%8a%80%e6%9c%af%e4%b8%8e%e5%ae%9e%e6%88%98/30%20%e7%9c%9f%e7%9a%84%e6%9c%89%e5%bf%85%e8%a6%81%e5%86%99%e5%8d%95%e5%85%83%e6%b5%8b%e8%af%95%e5%90%97%ef%bc%9f.md