像大神一样写代码之代码质量控制

Mofei超爱开源,最近接触了不少大神的开源项目,特别羡慕这些项目的代码质量控制,再加上公司最近也在强调代码质量,在挖坑、填坑的路上总结出一些经验和大家分享。

今天想聊的是其中的持续集成、单元测试和代码覆盖率的部分。

article image from 'zhuwenlong.com'

从我个人的角度来看,虽然单元测试和代码覆盖率会在一定程度上降低代码开发的效率,但是对于多人协作的项目,其带来的好处远远要大于开发成本。毕竟我经常遇到狗血问题,不想你也因为改了某一个你认为安全的代码然后导致了另外一个完全你认为没有什么关系的地方直接无法运行了,这种事情就像你把自己家门口的树砍了,然后却被FBI通缉一样不可思议。有了代码质量控制,你完全可以在上传代码之前就知道你的代码会不会带来不可思议的后果。今天我们以一个无聊的JS库作为示例https://github.com/zmofei/emojily 。之所以说这个库无聊是因为这个库可以把任何字符串转成emoji表情,比如你好=>😙😀😑😓😟😀😓😜😂😉,而且你还可以把这个表情转回文本,但是!包括其作者Mofei在内都不知道这样的一个库会有什么用……

我们先从代码提交之后发生的事情开始说起。

0. 持续集成 Travis

如果你认为代码提交到Github之后你的工作就完成了的话,那么太好了,你可能就是我要忽悠对象🤪。

0.1 什么是持续集成

事实上,编写代码只是我们开发工作的一小部分,在代码编写完成之后,你需要考虑的是如何构建、测试、部署等问题。其实持续集成也就是指这些在你代码提交之后所发生的事情。目前市面上有非常多的持续集成工具,我们这里选用github作者最爱的Travis CI作为我们这次探讨的基础工具,当然你也可以轻松的把它换成你熟悉的持续集成工具。在我们的这个例子中,我们用Travis来完成代码提交后的单元测试以及代码覆盖率的检查工作。

Travis from 'zhuwenlong.com'

0.2 如何使用Travis

打开Travis的官网,用你的Github账号登录之后,就可以链接一些你github中的项目了。Travis CI会检查你项目根目录中的.travis.yml文件,这个文件被称为Travis的配置文件,用来描述告诉工具在你提交代码之后如何执行集成工作,具体的语法可以参考Travis的官方文档

我们以我们项目中的.travis.yml文件为例:

language: node_js
node_js:
  - 8
after_script:
  - npm run upload-coverage

这里我们配置非常简单,告诉Travis我们使用的是v8版本的node.js,在脚本执行完成之后运行npm run upload-coverage脚本,这个脚本我们会在下文来介绍。

有了这个配置文件,我们在每次上传代码之后,Travis就会接管后续的工作了。现在万事俱备就只欠东风了!我们看看单元测试。

1. 单元测试 npm test

所谓单元测试顾名思义,就是对软件中最小可测试单元进行检查和验证。拿自行车🚲为例,如果我们想对自行车进行单元测试的话,那么我们就可以把自行车拆成多个小部件,车轮、车座、链条等,然后我们分别的测试这些部件是否工作正常,如果这些所有的零件工作正常了的话,我们就可以认为这辆自行车大概率工作正常,请注意这里说的是大概率不是一定,万一有人把前后车轮装反了呢?

知道什么是单元测试之后我们来关注一下如何进行单元测试。

1.1 单元测试如何运行

在Node.js环境中我们可以在项目根目录直接运行npm test来启动我们的测试脚本,node.js还很贴心的给test起了一个别名tst,所以如果你运行npm tst也是一样的。现在恭喜你知道了如何启动测试,但是别高兴的太早,现在如果你在一个新的初始化项目(通过npm init初始化)中直接运行npm test的话会大概率看到以下的结果:

> echo "Error: no test specified" && exit 1
Error: no test specified
npm ERR! Test failed.  See above for more details.

报错了?!!这是因为,在初始化的项目我们没有进行任何test的脚本的编写,而系统默认初始化的测试脚本又是这样滴:

 "test": "echo \"Error: no test specified\" && exit 1"

运行之后自然会保存了。所以我们现在要改掉他!但是,这个测试的初始化脚本又在哪里呢?我们尝试打开项目根目录中的package.json文件,可以看到里面有一个字段叫scripts,而且test命令也在其中。所以嘛~ 没错,你猜对了!npm test命令就在这里。

npm test 其实是 npm run test 的简写,而npm run test(test可以替换成任何变量),的意思就是找到package.json中的scripts字段,然后运行对应的test(或者你写的其他名字)中的指令。

1.2 如何写单元测试

了解了如何运行单元测试之后,我们就可以着手去写了单元测试了。单元测试的运行原理很简单,只要运行的脚本没有抛出异常并且正常返回的话,就算单元测试运行成功。

所以,正常的单元测试的要做的事情是:我们拿到对应的方法、类或者其他最小的运行单元,然后传入参数把他运行起来,再去判断运行结果和我们预想的是否一样。原理很简单,但是我们知道运行结果返回值是非党多变的,有时候的是空,有时候是JSON,有时候是数组,有的时候其他的格式,所以我们需要有一种机制或者工具能够很方便的判断出这个返回值是否和我们的预期一致,这个项目中我们选用了tape(npm install tape)作为我们测试的工具,它提供了各种判断的接口,可以让我们很轻松的写出各种测试方法。当然了,如果你有足够的时间和精力,愿意把每一个返回值的判断和每一个测试的提示文本都自己写的话,也是可以的,但是在很多时候这样做是非常低效的。

在这个项目中我们写了一个简单的测试文件index.test.js放在了/test目录中。简单拿出一段进行说明:

test('test decode with error input', (assert) => {
    assert.equal('Error Input, Please do not try to change any character!', decode('😈😀😀😀😀😜'));
    assert.end();
});

这里test('description', callback)中的test表示这是一个test任务,description则为这个任务的说明,你可以写上任何你要的描述文字,callback则是测试的主体内容。callback接收了一个参数,我们可以用这个参数的equeal或者其他的end之类的方法来对这个任务进行描述,具体的就不在这里赘述了(如果有需要的同学可以给我留言,如果需要的同学比较多的话,我可以单独写一篇文章介绍tape)。

写完test之后我们尝试去运行它:

node test/index.test.js

然后你就能拿到这样的结果:

ok 1 should be equal
# test decode with error input
ok 2 should be equal
# test decode with error input
ok 3 should be equal
# test decode with changed input
ok 4 should be equal
# test commend from slack
ok 5 should be equal

1..5
# tests 5
# pass  5

# ok

如果有错误的话你会看到这样的返回:

ok 1 should be equal
# test decode with error input
not ok 2 should be equal
  ...
# test decode with error input
ok 3 should be equal
# test decode with changed input
ok 4 should be equal
# test commend from slack
ok 5 should be equal

1..5
# tests 5
# pass  4
# fail  1

这个例子中我们让第二条的测试返回失败的结,从这个返回的结果中我们很容易的发现二个测试任务失败了,提示not ok(最后几行我们可以看到,这里一共有5个测试任务tests 5,通过了4个pass 4,有一个失败fail 1

现在我们就可以把这个脚本放在package.jsontest里面,并通过npm test运行了:

"scripts": {
    "test": "node test/index.test.js",
}

如果你有意的话,你会发现我们现在的test中是一条测试命令,有心的你或许会问,如果我们有多个测试文件,并且想执行这多个测试文件该怎么办呢?这个问题我们也可以通过tape来解决,我们把上面的命令修改成:

"scripts": {
    "test": "tape test/*.test.js",
},

这样当你运行npm test的时候,tape就会寻找test目录下所有以test.js结尾的文件了。

写好了test的脚本,每次上传代码到github之后,Travis都会自动的运行测试脚本,如果成功了就会给你的commit打上一个绿色的对号,如果失败的话,自然会毫不留情的给你打上一个小差差。

commit result from 'zhuwenlong.com'

恭喜!到目前为止,你已经学会了如何写测试文件,并且如何让持续集成来自动的运行测试指令。

但,这样就结束了么?

2. 代码覆盖率 codecov.io

事情远远没有我们想象的那么简单,试想,如果Mofei有个小弟,在Mofei的威逼下很不情愿的开始写单元测试,为了蒙混过关,这个机智的小弟直接写了一个这样的脚本:

test('go to hell test!!', (assert) => {
    assert.ok();
});

他想这样的脚本会在每次运行完成之后直接返回成功,然后,每次提交不就能骗过Travis了么?

当然Mofei也不是好惹的!他立刻想出了对策,这时代码覆盖率就派上用场了!

代码覆盖率确切的说应该叫测试代码覆盖率他能很好的反应出单元测试的质量。

1.1 如何进行代码覆盖率检查?

想要进行代码覆盖率的检查,我们可以使用一些已有的工具,这里我们引入nyc(npm install nyc),他可以很方便的产出一份代码覆盖率的报告。

安装好了之后,我们直接在package.json中添加这样一个命令:

"scripts": {
    "coverage": "nyc --reporter html tape test/*.test.js",
},

其中前半部分 nyc --reporter html 的意思是使用nyc并且导出一个html的报告(关于这个工具的用法,你可以查看官方文档或者在留言中告诉我,有必要的话我会写一篇文章去介绍这个工具),后面的tape test/*.test.js你懂的,就不用我啰嗦的说一遍了。

然后,我们可以尝试去运行npm run coverage(这里注意,不能运行npm couverage因为其他的脚本没有test那么霸气),运行完成之后你就会发现你的目录中多了一个coverage的目录,尽情的打开它,找到src/index.html,然后用浏览器打开。 映入眼帘的应该是如下的图:

coverage img from 'zhuwenlong.com'

PS:这是来自Mofei的一个非常好的示例,所有的测试通过率都是100%,要向Mofei学习哦!

但是,下面是Mofei的小弟写的一个不标准的单元测试:

coverage img from 'zhuwenlong.com'

这里我们看到encode.js的覆盖率惊人的只有18.18%!!(扣工资!扣工资!)

那具体是哪里写的不够完整呢? 我们点击encode.js

coverage img from 'zhuwenlong.com'

是不是鹅妹子嘤?!所有没有被单元测试走过的语句都被标红了!这下Mofei小弟不能偷懒啦,只能乖乖的去提高他的代码覆盖率了,哈哈哈!

在实际的项目中,如果你的代码率能达到百分之百的话,那将是非常非常棒的。但是这样会耗费我们大量的精力,甚至有时候我们根本就达不到百分之百。所以在实际的项目中,我们可以设定一个能接受的阈值,比如说90%、85%等。当然了,这要根据你的具体的团队情况来做决定。

到这里我们已经可以通过工具来检测我们的单元测试的质量了,千万不要以为这样就走到头了哦,其实事情还能更好玩儿!

1.2 如何结合github使用codecov.io

想想看,如果我们每次都得运行npm run coverage是不是有点麻烦?我们能不能更变态一些呢?比如每次提交代码之后,让系统自动的运行coverage?联想到我们之前做了持续集成的工具,其实我们可以实现这样的事情的。

介绍一个好帮手codecov.io,他不仅仅能满足我们这样变态的需求,而且还是一个机灵的小伙子,在你每次提交PR的时候,都会过来跟一贴告诉你当前的代码质量如何。

coverage img from 'zhuwenlong.com'

是不是很变态?! 接下来我们一起来调教TA吧!

打开https://codecov.io使用github登录。找到Add new repository按钮(我知道这个按钮隐藏的有点深,但是你一定会找到的),然后选择一个Github中的项目,这时候系统会给你一个Upload Token,有了这个Token你就能证明你是你了,然后就可以在任何地方凭借着这个Token往你的codecov中上传报告了。

coverage img from 'zhuwenlong.com'

想要上传报告,我们需要安装官方工具codecov(npm install codecov),然后在package.json里面写上一个上传的脚本:

"upload-coverage": "nyc report --reporter json && codecov -f ./coverage/coverage-final.json"

前半句nyc report --reporter json是用来产生一个json格式的报告,后半句是用来上传到codecovcodecov -f ./coverage/coverage-final.json"

现在我们只要运行:

CODECOV_TOKEN="21639620-4627-42ef-a21f-f270c6358671" npm run upload-coverage

就可以将你的代码报告上传了。

但是!

每次都需要输入这个Token是不是有点不爽?我们能不能让我们的好伙伴Travis来解决这个问题?答案是可以的,还记得我们的Travis配置么? 我们在after_script加了npm run upload-coverage

after_script:
  - npm run upload-coverage

但是仔细看的话你会发现,Token呢?为什么这里不用写呢?那么他是怎么知道我们的权限的呢?

试想如果我们把Token写在了配置里面的话,这就意味着每个人都能看到我们的token并且能盗用我们的权限,这样做是十分不安全的,所以Travis想到了这一点,给了我们一个添加系统变量的机会。

想要设置系统变量,你需要打卡Travis的网站,找到设置

coverage img from 'zhuwenlong.com'

把你的Token填入到Environment Variables

coverage img from 'zhuwenlong.com'

这样,Travis在每次运行upload-coverage的时候,就能自动的填入Token了。

大功告成!

让我们上传一次代码到github上看会发生什么吧!

coverage img from 'zhuwenlong.com'

上传成功之后,你会在commits列表中找到状态标识,你可以点击Details查看细节。在细节里你能看到每一次执行测试的详细日志,以及你的代码覆盖率的记录。甚至你能看到一个非常漂亮的代码覆盖率的图表。这里卖个关子我就不贴图了,大家自己去发现吧。

最后,告诉大家一个黑科技,别忘了把小图标加入到你的Readme里面哦,这些小图标会随着你每次提交代码而进行改变,让你的Repo看起来更加高大上哦!

[![Build Status](https://travis-ci.com/zmofei/emojily.svg?branch=master)](https://travis-ci.com/zmofei/emojily) 
[![codecov](https://codecov.io/gh/zmofei/emojily/branch/master/graph/badge.svg)](https://codecov.io/gh/zmofei/emojily) 
[![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg)](LICENSE) 
[![npm](https://img.shields.io/npm/v/emojily.svg)](https://www.npmjs.com/package/emojily)

最终的结果是:

coverage img from 'zhuwenlong.com'

每次当你提交代码的时候,对应的build的状态以及codecov的百分比都会根据此次运行的结果展示在页面上,这样任何人都可以在项目的主页第一时间看到你最新的单元测试是否通过,并且可以知道你的单元测试的质量如何,是不是很酷?赶紧去试一试吧!!!

Write a response...
Mofei Zhu
publish