为什么你需要写单元测试?

前言

维基百科中对单元测试的定义是这样的:

计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

但在日常开发中,大多数情况下,可能开发需求都排得满满的,哪还有时间去写单元测试?在我工作的前几年,基本上也没有写单元测试。单元测试只是学习了解而已。直到,我遇到了一个由于bug引起的重大事故。事后的反思,让我开始关注到代码质量的问题。

有没有办法提前发现这类问题?如何防止这类问题再次发生?如何提升代码质量?

面对上面的灵魂拷问,我对如何写好代码进行深刻的反思。写完代码,并不代表,你已经做完了,做好了,代码质量才是程序员的核心竞争力。测试通过的代码,才能有信心交付。单元测试才是有效提升代码质量的最简单可靠的方式。

为什么要写单元测试

对自己的代码有信心

修改代码后通过单元测试,说明修改的部分并没有破坏测试用例,这能增加对代码的交付信心。起码你有底气交给测试的同学去测试你的代码。但也要了解,通过单元测试并不意味着没有问题,有一些bug并不是单元测试的问题。单元测试关注的一个小的单元,整体功能的完整性并不能通过单元测试代替集成测试。

代码重构

很多时候,写业务代码,时间有很紧张,想要日后优化重构的话,没有写单元测试用例,根本就是寸步难行。很可能重构后,代码会不小心破坏掉之前填的坑。所以开发一般不会去重构现有的代码。但如果有单元测试的话,就不一样了,对现有的功能重构,建立对应的测试用例,改完代码,跑一边单元测试,如果测试都通过了,那么就代表重构并没有破坏原有的逻辑正确性。只有单元测试质量高,覆盖率高,基本上你可以随时重构优化你的代码。当然,单元测试依然适用于小的单元,重构一个函数,一个类,都是可以的。比如将当前的项目重构程为微服务,这时可能就需要重写单元测试了。

熟悉代码

单元测试通常会包含较多业务异常的测试用例,通过单元测试可以了解到需要注意的点,哪些业务是如何运行的,你不需要深入阅读代码就可以了解代码是做什么的,也是一个熟悉业务和代码的好办法。

怎么写单元测试

通常情况下,我们都是在已有的项目下进行单元测试的编写。单元测试都会有对应开发框架,以PHP为例,我推荐PHPunit,它是面向程序员的单元测试框架。在已有的项目加入phpunit,很简单。你需要以下3个步骤:

  1. 在项目加入配置文件phpunit.xml
  2. 安装好composer,并添加phpunit依赖
  3. 添加自定义引导文件

phpunit.xml

通常phpunit.xml的模板如下:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="cases/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./cases/</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">cases/</directory>
        </whitelist>
    </filter>
    <logging>
        <log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
    </logging>
</phpunit>

常见选项参数解释:

  1. bootstrap 定义你的引导文件,通常是你的框架加载类。
  2. colors 为true 可在终端显示颜色
  3. convertErrorsToExceptions 转换Fatal Error为异常
  4. convertErrorsToExceptions 转换注意Notice为异常
  5. convertWarningsToExceptions 转换警告warn为异常
  6. processIsolation 为每个测试运行在单独的进程增加隔离
  7. stopOnFailure 出现失败后是否停止运行测试用例
  8. testsuites->testsuite 通过name参数区分不同的测试用例,需要配置搜索用例的目录
  9. filter->whitelist 配置代码覆盖率的白名单
  10. logging->log->coverage-text 配置代码覆盖率的输出为php 标准输出,不显示白名单文件

composer

通常安装composer很简单

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

通过composer安装phpunit

#php7.0 - 7.2
composer require --dev phpunit/phpunit ^6
#php7.1 - 7.3
composer require --dev phpunit/phpunit ^7
#php7.2 - 7.4
composer require --dev phpunit/phpunit ^8

引导文件

有几种方式添加你的引导文件,第一种直接在命令行添加:

phpunit --bootstrap cases/autoload.php

第二种,直接在phpunit.xml配置参数

bootstrap="cases/autoload.php"

该文件用于加载常用的框架,公用库,函数等。

TDD

在日常工作中,使用单元测试的工作方式有3类:

  1. 从一开始就编写对应测试用例,也就是测试驱动开发,这其实就是TDD。
  2. 第二种是,先编写部分的代码,然后再编写对应的测试用例。
  3. 第三种,编写完代码,再编写测试用例。
分类 优点 不足 备注
TDD 不需要了解小单元的业务逻辑 关注点更多是在测试上,无法深入了解业务逻辑
T&C(Test & Code) 实时性强,写完一段代码就运行下测试,通过测试很有成就感 实现的同时就需要考虑,如何更好的测试复用
TAC(Test After Code) 符合日常开发流程 忘记写测试用例,重新梳理思路

实践经验

在实践单元测试的过程中,有一些经验可以供你参考。

什么时候需要写单元测试

如果每个函数方法都去写单元测试的话,时间可能根本来不及。建议以下代码进行单元测试:

  1. 核心业务逻辑。阅读、充值、购买等。
  2. 逻辑比较复杂的。
  3. 容易出错。
  4. 不容易理解的代码。
  5. 公共库文件。

测试用例

最好的情况下,是把测试用例与代码一起管理,这样可以方便阅读测试与被测试的代码。在进行代码部署时,可以不需要发布测试用例。如果想要做持续集成,也可以单独管理测试用例。建议的目录以模块进行组织,避免出现一个文件几千条测试用例的混乱情况。下面是一个参考的目录结构:

➜  tests git:(master) tree
.
├── bin
│   ├── release.sh // 发布脚本
├── cases
│   ├── APITest.php // 单元测试基类
│   ├── AppTest.php // App帮助函数
│   ├── autoload.php // 自动加载框架
│   ├── modulea // 模块a
│   ├── moduleb // 模块b
│   ├── modulec // 模块c
│   ├── moduled // 模块d
│   ├── modulee // 模块e
│   └── modulef // 模块f
├── composer.json // composer加载配置文件
├── composer.lock // composer锁文件,锁定安装时的版本
├── phpunit.xml // phpunit单元测试配置文件
└── README.md //单元测试文档

经验之谈

  1. 单元测试不是越多越好,而是越有效越好!
  2. 单元测试,最好是与实现代码同时进行。
  3. 单元测试需要覆盖核心业务逻辑等。

总结

在这篇文章,总结了从自己不会写单元测试到开始学习单元测试的经验,对比了3类单元测试的开发方式,分享了单元测试的实践经验。如果可能的话,可以将单元测试加入到日常的持续集成中,大大提高构建出来的代码质量。

程序员使用单元测试是提升代码质量最简单可靠的方式。

发表评论

电子邮件地址不会被公开。 必填项已用*标注