<audio id="audio" title="30 | 真的有必要写单元测试吗？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a4/32/a4934a46d694862967c94b6ed3a0f132.mp3"></audio>

你好，我是景霄。

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

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

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

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

## 什么是单元测试？

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

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

说起单元测试，就不得不提 [Python unittest库](https://docs.python.org/3/library/unittest.html)，它提供了我们需要的大多数工具。我们来看下面这个简单的测试，从代码中了解其使用方法：

```
import unittest

# 将要被测试的排序函数
def sort(arr):
    l = len(arr)
    for i in range(0, l):
        for j in range(i + 1, l):
            if arr[i] &gt;= 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=&quot;custom_val&quot;)
        a.m3 = MagicMock()
        a.m1()
        self.assertTrue(a.m2.called) #验证m2被call过
        a.m3.assert_called_with(&quot;custom_val&quot;) #验证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 &lt; 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/](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] &gt;= 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，而是适用于任何语言。所以，单元测试必不可少。

## 思考题

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


