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

前言

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

计算机编程中,单元测试(英语: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类单元测试的开发方式,分享了单元测试的实践经验。如果可能的话,可以将单元测试加入到日常的持续集成中,大大提高构建出来的代码质量。

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

Git工作流程实践

前言

上篇文章说到,选择Git作为版本控制系统,就是选择一种新的团队协作方式。Git常用的工作流程有几种:Git Flow、GitHub Flow、GitLab Flow。这三种工作流程几乎都是以功能驱动开发。先是有需求,才开始进行开发,建立对应的功能分支,完成开发后,该分支就合并到主分支,然后被删除。阮老师有一篇文章讲述得很清楚,这里我只是讲自己实践下来,觉得比较好的一种模式:GitLab Flow New。

为什么使用GitLab Flow New工作流程?

这里,我先讲讲为什么没有采用Git Flow 与 GitHub Flow的工作流程。

Git Flow的问题在于你需要花费时间去维护一个开发分支。

大部分的人都需要从开发分支新建分支,如果人数超过3个以后,你会发现需要去解决日常工作中的异常多的代码冲突。因为可能很多人,在新建的分支上进行开发,但合并开发分支的更新,却是在完成功能开发,这时候开发分支已经累计了无数的代码更新,再去合并的话,可能冲突会很多。

GitHub Flow的问题在于,你需要发起一个Pull Request,简称PR。你的PR通过了评审和讨论后,就合并到master里。可实际上合并到master的代码,也不一定是线上最新的代码。实际工作里,可能还会有预发布这一环节。

如何优雅使用GitLab Flow New工作流程

实际上,我们是这样使用GitLab Flow的。

首先定义3个主要分支。master为生产环境的分支,与线上代码保持一致。预发布的分支为prelease。开发测试分支从master里新建分支进行功能开发。

分支 作用 备注
Master 主分支,与生产环境的代码保持一致
Prelease 预发布分支,与预发布环境的代码保持一致
fea/xxx 开发分支从master创建分支

当你的功能开发完成并通过测试后,会新建一个MR(Merge Request与Pull Request类似,这是GitLab 里的叫法),请求合并到prelease分支。

发起MR后,就可以通知相关人员进行代码的Review,代码审核和讨论,没有问题后,会尝试合并到prelease。

这时自动构建工具会自动发布一个版本到预发布环境中。与此同时,根据这次改动将全部的代码打包一个全量/增量的zip压缩包,并请求发布系统获取标识唯一的版本号码,比如20190607_130102_9159

相关的开发和测试人员可以进行预发布环境的测试,当通过测试验证后,在发布系统对上述的版本号码进行发布,这样就更新到全量的线上机器了。

这里用一张图,说明该工作流程的步骤。
Git工作流程实践

常用技巧

Merge Request

我们采用GitLab的Merge Request机制进行代码Review,每一个新功能必须通过MR才能合并到prelease以及master。与GitLab Flow的上游优先,持续发布的原则类似,开发分支的代码只有在开发/测试环境通过了测试,才会允许发起MR合并到prelease。唯一例外的是,线上紧急问题的修复,是可以跳过这个流程的。

采用MR后,会利用no fast worword合并,并生成一个对应commit id,这样实现了代码线性的提交,也方便后面代码回滚。确认代码合并时,通常会选择合并后自动删除源分支的选项。

合并prelease的更新

开发人员提交MR后,可能会遇到prelease已更新,也就是落后提交的问题(x commits behind),这时,采用rebase的方式将prelease分支的代码与自己分支进行合并,并重新提交。

举例,当前你发起了一个MR,分支为fea/new,在merge_requests界面发现落后prelease几个提交,那么正确的方式是:

# 切换到prelease分支
git checkout prelease
# 拉取对应的更新
git pull --rebase origin prelease
# 切换到开发分支
git checkout fea/new
# 合并prelease的更新
git rebase prelease
# 推送到远端分支
git push -f origin fea/new

合并提交

为了方便他人阅读你的代码,也会要求开发人员将多次简短的提交,合并一个完整注释的提交,方便代码review。

举例,你会发现有这样的提交记录:

* 调试功能a x7 commitid7 author1 2019-06-15 17:00:07
* 调试功能a x6 commitid6 author1 2019-06-15 17:00:06
* 调试功能a x5 commitid5 author1 2019-06-15 17:00:07
* 调试功能a x4 commitid4 author1 2019-06-15 17:00:06
* 调试功能a x3 commitid3 author1 2019-06-15 17:00:07
* 调试功能a x2 commitid2 author1 2019-06-15 17:00:06
* 添加功能a    commitid1 author1 2019-06-15 17:00:07

你应该把这些提交合并到一起:

git checkout fea/new
git reset commitid1
git add .
git commit -m "+ 添加功能a,完成测试,参考下面的注释模板"
git push -f origin fea/new

Protected Branch

我们是会把prelease以及master进行保护,只有管理员、拥有代码审核权限的人员才会允许进行代码的推送。开发人员只允许推送自己的开发,测试分支。这样会避免prelease以及master主干分支的代码被不小心”污染”,也就是没有通过测试的代码提交到主干分支中。

注释模板

一个较好的开发习惯是提交详细的注释。详细的注释有几个好处:

  1. 提交更多的提交信息
  2. 方便快速浏览查找。
  3. 可以直接生成改动日志。

我常用的几个小技巧:

  1. 提交信息前添加固定字符代表增删改查,比如* 代表更新 + 代表添加 – 代表文件删除。
  2. 代码注释每行限制72个字符,方便过长被自动截断。
  3. 最后空出一个行,让上面的行作为标题,再下一行提供相关的链接和关键字。

一个简单的模板如下:

* 更新 写作 Git工作流程实践

整理自己在日常工作中实践下来,有效的的Git工作流程。

还有他人提供的常用提交模板,你也可以参考https://gist.github.com/jmaxhu/8e7fb69a7dcec1b9b953、

合并最新的代码

将自己的开发分支合并master更新的代码。这是一个很重要的操作。每天上班第一件事,就是拉取最新master分支的代码,采用rebase方式,合并自己开发分支的代码,保证自己的代码提交是线性的。这样在后续代码合并会显著的减少很多不必要的冲突合并时间。

总结

在这篇文章里,我分享自己在Git工作流程的一些尝试。定义工作流程不是目的,我认为最终目的是提升整体团队的工作效率以及避免经常遇到的”代码覆盖”问题,进而引起的”环境问题”。解决在日常工作可能会遇到的问题,才是我们最终目的。

每个团队的使用习惯,业务模式,可能都不相同。一种工作流程可能也无法满足所有人的需求,大家可以根据自己的习惯或者最适合当前团队的方法去尝试,去定义适合自身团队的工作流程。

Git工作流程实践

SVN迁移Git实践

前言

Git是我工作使用得最多的代码管理工具。从毕业到现在,一直在使用Git管理我的代码,使用Git的branch分支功能进行需求开发,尝试使用GitLab进行持续构建。关于Git与SVN的对比,你可以阅读这里的文章。大多数人,可能会认为SVN已经很好的满足我的工作需要,为什么还需要去迁移到Git呢?

为什么要迁移到Git

必备技能

Git 是最流行的版本管理工具,也是程序员的必备技能之一。未来可能SVN也会变为历史。学习Git后,你可以尝试在终端命令行操作Git。使用命令行的操作几乎可以完成大部分操作,极大的提升你的工作效率。除了代码对比之外,几乎不需要GUI工具了。

分支合并

SVN的分支合并是一个老生常谈的问题。使用Git里,你可以创建无数的分支,尝试自己各种创意的想法,把代码合并到master里,也不是一件很复杂的事情。但SVN不是的,尝试过Git后,再使用SVN时,每次合并都会让你感到无力,切换分支需要花费时间,合并分支需要时间。时间对工程师来讲,可能是开发撸代码中的最大成本。

团队协作

团队协作是Git的一大特色。我认为Git是间接让GitHub已经成为最大的程序员社区网站的原因之一。假设有一个推荐系统的源代码,如果你觉得其中的算法,无法满足自己的需求,手动git clone下来,尝试建立单独的分支,尝试自己的想法,通过无数的单元测试后,对自己最后的代码调整完毕,发起一个MR说明自己的改动原因,希望自己的提交尽快可以被Review。选择Git作为版本控制系统,就是选择一种新的团队协作方式。如果要使用SVN的话,我不知道要如何去实现分布式的代码提交体验,甚至无法想象SVN的协作流程可以有这么顺畅。

好了,说了这么多,那么如何将SVN迁移到Git呢?

如何将SVN迁移到Git

准备一份提交者列表

通常需要先整理svn的全部提交者的username和email,与git进行对应。

svn log --xml |\
grep '<author>' | sed 's#<author>##' | sed 's#</author>##' | sort -u |\
sed 's/.*/& = & <&@test.com>/' > authors.txt

上面的命令为了获取全部的作者的的用户名和邮箱地址,我们来分解下每一步操作的作用:

svn log –xml 以xml的格式输出对应的日志
grep ‘‘ 搜索author为关键字的行,避免获取到注释中有author的情况
sed ‘s###’ 搜索行的替换为空
sed ‘s#
##’ 搜索行的
替换为空
sort -u 去重排序
sed ‘s/.*/& = & &@test.com/’ 将每一行进行格式化
author => author = author author@test.com

SVN版本库

假设有这样的一个SVN的版本库:

# svn url
svn+ssh://svnuser@svn.xbc.me/your_svn/
# 目录结构
branches
tags
trunk

那么最简单的将SVN版本转为Git版本库的命令为:

git svn clone -s svn+ssh://svnuser@svn.test.com/your_svn --authors-file=authors.txt

这样将SVN的全部代码对应起来:trunk对应到master,branche以及tag都对应到Git版本库。

实际上,我们在迁移的过程中,可能遇到另外一类情况。假设有以下SVN的版本库:

# svn url
svn+ssh://svnuser@svn.test.com/your_svn/trunk/
# 目录结构
app
h5
pc

那么我们尝试将单一的版本库拆分为3个不同的版本库:

git svn clone svn+ssh://svnuser@svn.test.com/trunk/app --authors-file=authors.txt --no-metadata
git svn clone svn+ssh://svnuser@svn.test.com/trunk/h5 --authors-file=authors.txt --no-metadata
git svn clone svn+ssh://svnuser@svn.test.com/trunk/pc --authors-file=authors.txt --no-metadata

使用–no-metadata 会移除类似下面的svn id信息。

git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de-
    be05-5f7a86268029

现在你可以添加远程分支,进行代码提交了。

git remote add origin git@git.test.com:my/app.git
git push origin master

同步SVN版本库的更新

在转为Git版本库后,可能会还有一段过渡期,也就是以SVN为主,待熟悉Git整体工作流程后才会切换到Git。那么这时,该如何同步SVN的更新到Git呢?

我会使用以下脚本进行代码的同步:

git checkout master
git svn fetch
git svn rebase 
git push origin master
git checkout -

主要的命令有2个:

git svn fetch 获取SVN版本库的更新提交

git svn rebase 将SVN的更新变基当前master代码分支,这样整体的代码提交就是线性的了。

最好把上面的做成定时任务去执行就好了,每10分钟执行一次。

*/10 * * * * bash git.up.sh

常见问题

svn: E220001: Item is not readable

出现这个问题,是由于svn log的命令参数错误导致的。

svn log ^/ --xml
改为
svn log --xml

Author: (no author) not defined in

出现这个错误,是因为在作者列表找不到该用户,需要手动添加该用户。

(no author) = no author <noauthor@test.com>

总结

将SVN转换为Git版本库,可能整体工作流程迁移中的一小步,更重要的整个工作流程是否支持Git版本库,项目的DevOps流程是否完善,比如持续集成,自动构建,代码检查,自动发布等。另外,我认为更重要的是学习并熟悉Git的开发协作流程,比如我经常使用到的GitLab Flow的变种。下一篇会尝试说明如何进行GitLab Flow的工作流程。

SVN迁移Git实践