相信各位读者肯定体验过持续集成(CI)吧。持续集成通常用来确保当前代码库的质量,反映软件开发的进度。有了持续集成后,程序员们提交代码也会变得更加小心谨慎。应该没有人乐意让组里其他同事不停地见到自己的分支上CI失败的邮件吧(笑)?
一个最简单的CI应该就是单纯地在模拟的生产环境下执行单元测试,然后报告单测的结果。
稍微复杂点的CI可能还会对代码复杂度等多项指标进行分析。其实,作为编码规范的度量尺、代码质量的把关者、项目健康的测量仪,CI可以做的事情还有很多,足以帮助减少code review(如果有的话)的负担呢,并时时警醒你不要写出敷衍的代码。
本文谈谈在持续集成中你可以用到的三板斧:linter -> unit test -> code smell reporter
linter可以对代码做静态分析,找到有问题的地方并报告。
unit test是持续集成的根基所在。
code smell reporter可以衡量项目代码的健康度,督促程序员们关注代码质量,让整个程序健康成长。
幸运的是,关于这三个领域,大部分都已经有了现成的开源工具。你所需的,也许是些微小的工作:把它们集成到你们项目的CI工作流中。也许只是往现有的CI服务中添加额外的几个新流程。假如你的同事不愿意接受这样的CI工作流,你也可以选择独自在自己本地运行它们。通过Git钩子,你可以在每次commit的时候把它们运行一遍。(其他版本控制系统估计也有相似的机制)
在本文的末尾,我会附上一些主流编程语言在这三个领域里常用的工具,以便读者入手。
linter
所谓的代码静态分析,就是使用linter来分析代码文本,找出一些浅显的问题。这些“显而易见”的问题包括未定义变量、未定义函数、未使用的变量、未使用的参数、不可达的分支等等。这些事情也可以放到code review的时候人工去做。然而交由专门的工具来完成,可以大大地节省人工review的时间。这样一来,code review的时候可以专注于接口命名、代码功能设计、逻辑缺陷、安全漏洞、可读性等机器目前无法胜任的工作。另外,人会失手,而机器不会。以前我看过一本类似于访谈录的书,里面有句留下了深刻印象的话,大意是“如果没有自动化的linter保证代码风格一致,就不要在这方面苛求他人,因为人人都有犯错的可能”。
编译器/解释器可以帮忙完成一部分的代码分析,比如gcc -Wall -Wextra -Weffc++ -Woverloaded-virtual
。也有用来完成更高阶的代码分析的专业linter。
许多的linter专门用来评判代码风格是否符合要求,如[pep8](https://pypi.python.org/pypi/pep8/)
。老实说,我并不认为代码风格是什么值得重视的事情。它之所以会被屡次强调,只是因为每个人都能轻易地对此评头论脚(相对于更隐晦的逻辑缺陷等问题而言)。我见过一些符合pep8规范的代码,它们对边界条件的处理还不如对换行的处理严谨。与其关注于某段代码是否合乎规格,不如关注于其他更实际的问题。当然了,实在写得太过于混乱的代码除外。
切回正题。大多数的linter关注于找出代码中的瑕疵的。虽然你可能会觉得,一般的程序员不会犯下诸如变量未初始化、编写不可达的分支这样的问题,但是人就是容易犯错,偶尔的typo也是在所难免的。所以,还是选择一个最聪明而又不饶舌的linter,让它督促你养成良好的编码习惯,矫正代码中的细微错漏吧。
某些时候,linter可能会作出“假阳性”的报告。举个例子,有些语言的回调函数要求提供某些入参,然而这些入参不一定在回调函数里用到。所以是否通过了linter可以不作为持续集成成功与否的前提,而是独立作为一份报告列在持续集成的结果里。当然如果你的linter支持复杂的配置,也可以在代码或配置文件里面调整linter的行为,比如提示linter在某段代码内关闭某个检测,例如@SuppressWarnings(...)
。这个就见仁见智了。
unit test
单元测试的重要性深入人心,应该没有必要再啰嗦一遍。毕竟单元测试是持续集成的根基所在,连单元测试都没有就勿论持续集成了。
单元测试的编写风格大致分成两个流派,TDD和BDD:
# TDD风格
class TestA < MiniTest::Test
include A
def test_arithmetic_one_equal_one
assert_equal 1, 1
end
end
# BDD风格
RSpec.describe A, 'test' do
context 'arithmetic' do # context是可选的
it 'one equal one' do
expect(1).to.eq 1
end
end
end
就我个人的看法,TDD和BDD没有什么不同。同样的测试用例,以TDD还是BDD的风格编写在击键次数上没有明显的区别,可读性也差不多。BDD对TDD的一个优势是,它的层级较为灵活。由于TDD往往是用一个测试类来对应一个实现类(用一个测试模块来对应一个实现类是可行的,不过到了这一步你该想想是不是实现类过于臃肿了),导致默认的层级只有两级:类和方法。如果想把若干测试用例独立分组,就要使用test_group_name_test_a
这样的长方法名。而BDD由于describe
和context
可以反复嵌套,在层级这方面没有限制。不过在实际应用中,多于三个层级的需求很罕见,所以BDD这个优势不明显。一般来说,对于测试框架通常是随大流。因为主流的测试框架会有更多的集成支持。如果没有主流意见,选自己熟悉和所需击键次数少的。
说到集成支持,测试框架所不能或缺的是对覆盖率的统计。如果测试框架自身不提供该功能,就要靠额外的工具提供支持了。单元测试会给人一种幻觉,就是以为有了单元测试就说明对接口行为的变更就一定是稳定的。过低的测试覆盖率比没有测试更加可怕。
软件开发中常常出现这样的场景:
开始项目:老大:“同志们,一定不能忘记写测试!” 其他程序员:“好的老大!”
项目又要延期了:其他程序员:“写单测占去了1/3甚至1/2的时间。现在工期紧,我就不写测试了,将来再补上吧。” 老大:“好吧。”
于是乎测试覆盖率渐渐往下掉。明明是新功能加得最多的时候,写的测试却变少了,到后来都算不上有单元测试了,各种变更捎来的BUG也慢慢潜伏起来。
面汤上就飘着两三片牛肉,好意思叫牛肉面?单元测试行覆盖率不到70%,好意思叫有单元测试?
总之,是否有单元测试并不重要,重要的是覆盖率可以达到多少。这一点在持续集成时一定要统计出来,作为一个指标提倡编写新功能的程序员也要编写同覆盖率的单元测试。
由于单元测试内各组件往往会被mock掉,要想真正保证各组件能协同合作,程序员们还需要编写集成测试。不过集成测试可能涉及较为复杂的环境配置,所以持续集成中一般不包含集成测试。
code smell reporter
我们来看看三板斧中最后一击——code smell reporter,专门用于报告代码中的“异味”。所谓的“代码异味”,指的是软件中一些可能导致深层次问题的迹象,如
过长的方法
巨型的类
太多的入参
不依赖成员变量的成员方法
某个方法过于依赖另一个类
冗余代码
过高的循环复杂度
等等。
code smell reporter工作方式跟linter很像,也是解析代码文本,生成抽象语法树,然后处理之。事实上,有些linter也提供了报告代码异味的功能。那这两者间有何不同?在我看来,code smell reporter会更加关注于类/函数层面上的缺陷,而且有些code smell reporter会始终如一地反馈相关数据,以便于对代码质量进行追踪。
正如软件工程中的其他指标一样,code smell也有其局限性。什么算是“过长”?什么算是“太多”?什么算是“巨型”?这些指标即使在同一个项目中,也会因功能的不同而不同。可惜的是,目前还没看到可以按不同的文件指定不同配置的code smell reporter。你只能按整个项目去配置它,而这会迫使用户忽略某些文件中的报告。随便提一下,许多code smell reporter默认的阈值太低,总要调高一下。
不管怎么样,虽然code smell reporter有其不足之处,但是总比完全不重视强。可以像linter一样,在持续集成报告中专门做一个页面来展示code smell reporter的输出结果。当然了,更为关键的是以此激励编码的时候重视代码质量,消除code smell reporter的抱怨。
附录
主流语言的部分开源工具(每种最多列一个,未在此列出不代表不推荐):
编程语言 | linter | unit test | code smell reporter |
---|---|---|---|
C | gcc -Wall -Wextra | Check | - |
C++ | g++ -Wall -Wextra -Weffc++ | GTest | - |
C# | Gendarme | NUnit | SonarQube |
Java | javac -Xlint | Junit | SonarQube |
Javascript | jshint | jasmine | plato |
Objective-C | clang -Wall -Wextra -Wmost -Weverything | OCUnit | oclint |
PHP | PHP_CodeSniffer | PHPUnit | PHP_CodeSniffer |
Python | flake8 | pytest | radon |
Ruby | ruby-lint | minitest | reek |
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。