做开源软件项目会用到的服务简介

一般,程序员做公司的商业软件项目都会用到各种工具,这里的工具是指,源代码的版本控制、持续集成等工具。但世界上很大一部分软件项目其实都是开源的(没有统计过,猜想而已),而开源软件项目,参与的程序员一般都分散在世界各地,没有集中的工作场所,自然也不会有统一的涉及软件配置管理等工具的托管场所。

那么,有没有好的解决方案呢,自然是有。即使没有的话,也会有一帮程序员做出类似的工具和服务的。比如,下面会介绍的几个部分服务,完全可以满足大多数开源项目的要求(Python项目就更多满足了)。这些服务都是免费的,当然要更好服务,那就得掏出一些美刀了。

当然,如果信任这些服务的话,很多公司的商业项目或初创公司而言,也完全可以用这些服务起步,只要支付一定的美刀就行,就能省去很多额外的麻烦。

注意:部分服务的网站可能需要翻墙。

源码托管

GitHub:https://github.com/

就不废话了。如果不想折腾成Mantis、Bugzilla、Trac、Redmine那种更为复杂的issues跟踪管理,也完全可以用来做开源项目的issues跟踪管理。

http://pm.readthedocs.org/_images/small-GitHub.png

持续集成

Travis CI:https://travis-ci.org/

很好结合到了GitHub,可以用GitHub帐号统一注册登录。

只要开源项目中,写好相关配置,就会自动按配置执行持续集成的相关过程,比如:单元测试,真正做到让你的每一次代码提交,都能得到测试。当然,前提是你得写单元测试代码啦。

有关单元测试的内容,具体可以看 单元测试

http://pm.readthedocs.org/_images/small-Travis-CI.png

代码质量检查

Landscape:https://landscape.io/

也是很好结合到了GitHub,可以用GitHub帐号统一注册登录。

只要开源项目中,写好相关配置,就会自动按配置执行代码质量检查,看看代码有没有错误啦,有没有“坏味道”啦,以及有没有违反约定风格之类的东西。

不过,这个服务只针对Python作为编程语言的项目,其它编程语言估计也有类似的服务吧(没有的话,估计也有一帮程序员正做着吧)。

http://pm.readthedocs.org/_images/small-Landscape.png

测试覆盖率检查

Coveralls:https://coveralls.io/

同样,很好结合到了GitHub,可以用GitHub帐号统一注册登录。

只要开源项目中,写好相关配置,就会自动按配置,根据持续集成中单元测试结果,得到测试覆盖率。

http://pm.readthedocs.org/_images/small-Coveralls.png

文档托管

Read the Docs:https://readthedocs.org/

这个帐号不能像上面一样用GitHub帐号通用得注册登录了,需要自己单独注册一个。

项目的文档只是得用Sphinx编写(对Python项目而言,Sphinx这个算是标配工具了),就可以很好的用这个服务了。

有关Sphinx的编写,具体可以看 用Sphinx编写技术文档

http://pm.readthedocs.org/_images/small-ReadTheDocs.png

最后

具体如何使用,这里就不介绍了,各个服务的网站上都有足够的文档说明。并且,这只是一片水文而已,所以不会有太多内容的。

上面几个服务,大多都提供徽章了服务,比如,可以直接放到GitHub项目中的README中,这样别人就可以看到你的项目持续集成是否通过、代码质量是否足够好、测试覆盖率是否足够高。例如,这个我正在写的一个简单的类似Hacker News网站的开源项目:https://github.com/akun/PyHackerNews

README中再放上一个项目文档的链接,就给自己的项目配备了很好的文档服务了(特别是很多类库性质的项目)。比如,这个《软件构件实践》系列文章就是用了这个文档托管服务:http://pm.readthedocs.org/

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.org。并且部分内容已经公布在GitHub上:https://github.com/akun/pm

如何进行code review?

code reivew是保障代码质量的实用方法之一,这里简单分享下个人code review的经验。建议只当案例来看,因为不同项目、不同团队所做的事情、所具备的技术背景也是不同的,当然也会有些通用的点。

现实情况

先问下,你所在的公司有code review的习惯不?你所在的团队呢?你个人呢?如果没有的话,为啥会忽略这个环节呢?

来说下现实情况,大多数公司做的项目,如果按项目规模来看,都是很小的项目,即使是很大的项目,正确的做法也必然会拆分成多个小项目,由多个团队同时合作完成。在这个前提下,实际每天每个程序员提交的代码量是很少的,对这么少的代码变更进行code review其实是花不了个人多少时间的。

我觉得完全每天可以抽出半小时,乃至一小时的时间来对别人的代码进行review,比如:

  • 或每天上班正式开始自己的工作前,边吃早点边code review别人昨天的几次提交。
  • 或中午大桌聚餐在大液晶屏,边吃边评论互相的代码
  • 或每天下班前花半个小时review下别人代码

只是随便举几个场景的例子,code review形式不限、时间、地点更无所谓。

不同的团队,不同的项目,在不同的时期,可能对代码发布的要求不同:

  • 有的团队,对代码质量控制很严格,会规定凡是提交到主干的代码都必须经过review,严格保持主干的代码历史提交都是足够干净的;
  • 但现实中也有很多团队,根据实际情况采用事后review,review后随时修正,不过这样看起来代码历史看起来不那么干净,但最后结果总还是不错的。

不管如何,我觉得做项目本身都没有十足的铁则,实用就好。

code review的目的

团队中为什么要有code review的文化呢?或者说,code review有哪些好处吗?

从项目角度,很直接,可以提高代码质量。从人性弱点上来看,人类总是乐于去发现别人不好的地方的,所以多一双眼睛,更容易发现代码中的坏味道;另一方面,大家内心深处都是乐于追求真、善、美,所以总是希望写出来的代码更加优美。间接上,代码质量提高,对于一个项目的成功,以及一个软件的生命周期的延长,是个挺必要的因素,至少微观上是这样。

从个人角度,可以大致分两种。对于初学者学习,从看有经验人的代码过程中,可以学到好的代码实践;对于有经验的人自省,自然能发现各种不好的,时刻提醒让自己避免这种不好的代码,顺便可以帮助别人更好更快地提高,毕竟大家都是从初学者过来的。

从团队角度,逐渐拉近团队成员间的水平。尽管团队中成员各有所长,但在代码层面会有共同标杆,共同进步,让团队整体实力得到提高。

code review的形式

code review的形式各种各样,这里按人数规模来分类。

1自审

这个其实是大家都在做的方式,但可能不一定每个人都能做得好,或者说人类总是很难自觉做到从自己身上发现问题,经过一定训练后逐渐会学会限制这种本能。一种方式可以有所帮助,就是找个木偶讲解代码,比如:放在桌上的瓦力小机器人或盆栽之类的东西。讲出你写的代码的变化,也许就会发现一些问题。

当然,一般都会借助一些工具来帮忙检查代码,比如:各种语言的lint工具或者很多IDE本身的功能,在代码风格这些偏静态层面的东西,可以辅助发现一些问题。

另外,随着程序员编程经验的增长,逐渐会注意到代码的各种坏味道,这也是给别人做code review的基础。

这种形式的code review,发现了问题基本上就是立马纠正了。或者说,自己已经处理了一大半的代码坏味道了,剩下可能真的自己都很难发现了,毕竟不是每个人的自省能力都是很高的,所以才有了后面的几种形式。

p2p互相

这种形式其实也很常见,特别是两个小伙伴一起协作编程的时候,比如:所谓的结对编程。当然,也可以不是结对编程,比如:两个人挨着坐,一个人完成一块逻辑复杂或稍有难度的代码后,需要别人来把把关,这个时候就可以叫上旁别的人帮忙过下代码或自己给他讲解下代码。

如果是结对编程,个人认为还是需要两人水平相当,一个人在一个显示器前写代码,另一个人在另一个复制的显示器前观察写的代码,有问题随时沟通,也许两人各有所长,一人完成某块他擅长的部分后,两人交换角色。这个时候,其实是个互相审核的过程。

当然,也有不在少数的人不习惯所谓结对编程,那么就各自干活,谁觉得需要另一个人帮忙,就进行code review。这种形式,个人觉得就不必要求两人水平相当,也可以一个老手+新手的组合。

另外,个人建议这种形式的code review,最好两人负责的任务是比较相似或有交集,效果会更好些,不然一个人不理解另一个人的业务逻辑,效果上还是会打些折扣的。

这种形式的code review,也是发现了问题基本上就是立马纠正了。如果这个时候提交代码,至少2个人看过了,或者说有2个人对这块代码负责了,也能从一定程度上对这块代码所具备的知识有了一份复制,软件这种东西的背后知识看不见摸不着,知识的复制备份尤其重要。

2-3单向

这种形式随着各种code review的工具出现,越来越方便,自然也就越来越流行了。这里的“2-3”意思并不是只有2个人或3个人,根据需要也可以再增加人数,特别是有很多code review工具辅助的时候。

这里特别强调是单向,说白了就是你提交了代码,别人来评论,赞一下或踩一下,当然写代码的人也不要把这些评论太过心,过脑即可,意思是说别人的评论都是善意的,别人好的建议或意见可以采纳,如果要拒绝可以向对方说明理由,就是觉得不爽时候别太放在心上。如果不习惯用各种code review辅助工具,可能就是比如:有2个人站在你后面,你指着显示器上的代码给他俩各种讲解,他们有疑问随时提问,你负责说明,也就是一问一答。如果有code review工具辅助,那就更方便了,大家直接看代码,然后在线对代码进行评论,比如:如果有2+以上人允许通过,那就可以提交,如果有人反对或有改进建议,就直接评论,各自的邮箱也能收到需要code review的任务和结果,那块代码的作者也可以根据评论来改进自己的代码。

当然,这个时候个人建议那2个人中,有至少一人是个老手,最好是那种什么扫地阿姨,背后一站,扫一眼就看出你这有个溢出那种。当然这是玩笑了,意思是得有个有经验的程序员把关。

另外,至于是否对这块代码有了解或熟悉,就可以不用像上一种“p2p互相”那种形式要求的那样,大家最好都要对这块代码熟悉了。

这种形式的code review,一般是由写代码的人负责记录问题,当然有code review工具辅助,那就工具自动给你记录了,省去很多麻烦。这种形式也是最省事的,特别是有了工具辅助,也就没有时间、地点的限制了。

>3展示

这种形式有点会议形式,更接近“代码审查”了,而不是“代码评审”,一堆人圆桌+大屏。如果有场景,的确要用到这种形式的code review那就要慎用了,大家都知道一旦变成开会形式了,那就必然会有会议的各种缺点出现,所以要注意。

  • 首先,得有个主持人,无特定人员要求,也可以是写代码的那个人,没有关系,关键这个主持人能控场,别让偏题了,毕竟人多口杂。
  • 其次,必须举手发言,不然容易变成聊天。
  • 最后,避免争论或者叫吵架吧,可以讨论或叫沟通吧,互相不要对着人说话,而是对着某个物件来说话,比如:前面提到的那个瓦力机器人和盆栽,这样能一定程度上避免出现争论或吵架的情况。

当然,一旦开会形式了,针对某个人写的代码,容易变成批评、批斗,所以还得要注意:

  • 不要去关注谁写的代码
  • 对代码不对人
  • 重点在业务逻辑或代码设计实现
  • 弱化代码风格的评价
    • 除非展示目的是为了大家了解代码风格
    • 除非代码风格让人没法进行代码的审查

另外,如果没有强烈的特别的需要,其实还是不要这种形式的code review了,主持人不好或大家意识上不到位,容易浪费时间,效果不一定好。

这种形式的code review,可以指定一个记录人,当然就和主持人不是一个人了,不然主持人即当爹又当妈,功能不要太全哦!当然最好可以是写代码的那个人。

其它

这里说些其它形式的code review。可以引入某方面的专业人士,比如:安全专家,对代码进行安全层面的审计,虽然程序员必须具备安全意识,但安全层面的思路有时候还是会有所差异的,毕竟每个人的安全意识水平也参差不齐;再比如,需要对数据库或操作系统等的特别操作,可以让这方面的专家帮忙把关,当然如果团队中有这些类型角色的成员,就再好不过了。

最后,提几个建议,可能上面说过:

  • 针对代码,不针对人。但另一方面代码毕竟人写的,所以需要注意评论的方式。
  • 提出评论的人,可以采用发问的形式评论,比如:是不是XXXX这样改,这块代码执行起来就会更加OOOO了?类似这样的语气
  • 被评论代码的人,别有抵触心理,要有空杯心态,就是上面说的过脑但别过心了。
  • 程序员这个群体,并不是所有人都是天才级别的,所以当你看到别人写的烂代码时候,也得知道自己当初也可能经历过那个阶段。

code review的重点

本来想说下,哪些地方需要重点code review,但发现场景不同,项目不同,采用的技术不同,大家的关注点都不一样,如果说成通用的,反而没有重点了。比如:大段代码、大函数、大类、长SQL、复杂存储过程、使用频率高的功能、代码嵌套太深、核心算法实现、核心接口实现、函数参数过多、用户输入校验是否有安全隐患、异常处理、日志记录、业务是否需要事物等等太多了。

另外,不同编程语言、编程框架还有不同规范性质的编程建议,实在不好说重点。

如果要看重点,估计还是自己去翻一遍《代码大全》比较实际。

code review的工具

code review有很多工具,最原始的莫过于人肉了,这里简单列举下几个工具,看各自实际情况使用了。一般对这种类型工具或软件的要求可能有:

  • 可以评论,跟代码关联程度高,然后最好能跟网易评论似的各种嵌套。
  • 可以邮件提醒。
  • 可以很好支持Git或SVN等版本控制工具。
  • 可以很好支持Jenkins等CI工具。
  • 可以支持命令行,方便写脚本集成其它工具。

列举下我知道的工具:

  • Trac插件:Code Comments。如果用的Trac管理项目,可以用下,但没有上面说的具备的要求那么全。
  • Review Board。一个Python写的Web类型工具。
  • Facebook Phabricator。小有名气,Facebook出品,应该人家内部自己做项目都用的这个,功能有点太全了,可能如果单做code review,会有点晕。
  • Gerrit。一个Java写的Web工具,可以和Jenkins很好结合,也算好用。
  • Code:豆瓣出品,没用过,但据说不错。

另外,用了工具后,别忘了还有最实用的面对面交流这个人肉工具,也许用惯了冷冰冰的工具外,小伙伴互相看完代码后,温暖的会心一笑还是不错的。

最后

  • code review多在平时的零散时间。
  • 贵在坚持、积累,让code review成为呼吸一样理所当然的事情吧。
  • 对代码质量有更多提高效果的其实是“code review的形式”说的平时的前3种情况。
  • 某种角度说,代码是产品、项目、软件的基石,还是需要关注下的。

有关code review的趣评(来自网上,出处不详):

  • 如果代码变更在几十行内,能写出一堆评论
  • 如果代码变更成百上千了,基本都一个评论,“你的代码不错~”

其实想表达的意思是,递交的代码让别人review不要太爆炸了,别人会很有压力的。

后续,有关code review的主题,会具体介绍下几个常用的code review工具的安装和使用。

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.org。并且部分内容已经公布在GitHub上:https://github.com/akun/pm

用Sphinx编写技术文档

大家会发现,如果一个项目主要是用Python写的,其文档都很类似,比如:Python在线的HTML官方手册。这些项目的文档都来源于一个很不错的项目:Sphinx。这个Sphinx特指Sphinx doc这个项目(另一个也叫Sphinx的search的项目,虽然都叫一个名字)。

官网:http://sphinx-doc.org/

以下出现Sphinx的地方,都特指Sphinx doc这个项目

使用场景

  • 很多开源Python库项目的API手册都是用这个写的,可以看Sphinx官网给的链接:http://sphinx-doc.org/examples.html
  • 如果是用Python写的商业软件,也可以用这个来写技术文档,纯文本管理研发文档,保证功能代码、测试代码、相关文档同时更新
  • 直接拿来写在线书。比如,这个《软件构建实践系列》就是:https://github.com/akun/pm
  • 直接用来做slide等演示幻灯片,从一定程度上替代PowerPoint。比如,http://example.zhengkun.info/slide.html

功能

这里就列举个人关心的几个特性:

  • 文本是rst格式语法
  • 生成HTML、PDF、Slide(需要插件)等格式的文档
  • 支持文档、代码等引用
  • 支持自定义样式
  • 支持插件扩展
  • 直接看官网手册了解更多:http://sphinx-doc.org/contents.html

语法简介

就是rst的语法,这里就列举几个常用的:

标题等级

rst如下:

一级标题
========

二级标题
--------

三级标题
^^^^^^^^

效果如下:

习惯上,可以用以下字符:“= - ` : ‘ ” ~ ^ _ * + # < >”。最好能约定个依次标题等级。

列表

rst如下:

* 列表1
* 列表2
* 列表3

效果如下:

  • 列表1
  • 列表2
  • 列表3

列表写法除了用“*”,还可以用:“-”,“+”,最后效果一样。

上面说的是无序列表,如果想用有序列表,可以用“#.”。

rst如下:

#. 列表1
#. 列表2
#. 列表3

效果如下:

  1. 列表1
  2. 列表2
  3. 列表3

表格

rst如下:

=====  =====  =====
第1列  第2列  第3列
=====  =====  =====
8      1      6
3      5      7
4      9      2
=====  =====  =====

效果如下:

第1列 第2列 第3列
8 1 6
3 5 7
4 9 2

插入图片

rst如下:

.. image:: images/ball1.gif

效果如下:

http://pm.readthedocs.org/_images/ball1.gif

插入代码

展示代码示例,经常会用到:

默认

rst如下:

::

   print 'Hello World!'

效果如下:

print 'Hello World!'

自定义

rst如下:

.. code-block:: python
   :linenos:

   print 'Hello World!'

效果如下:

1
print 'Hello World!'

引用代码文件

rst如下:

.. literalinclude:: code/example.js
   :language: javascript
   :linenos:

效果如下:

提供下载文件链接

直接下载该RST本身。

rst如下:

:download:`sphinx.rst <sphinx.rst>`

效果如下:

目录索引

example1对应sphinx.rst所在目录下的example1.rst文件,example2类似。

rst如下:

.. toctree::
   :maxdepth: 2

   example1
   example2

效果如下:

  • 二级标题1
  • 二级标题2

引用

可以用于跨rst文档间的内容互相引用。这里以本文档内为例。

rst如下:

.. _my-reference-label:

用Sphinx编写技术文档
====================

很长的文字内容

点击回到顶部, :ref:`my-reference-label`.

效果如下:

点击回到顶部,  用Sphinx编写技术文档 .

文字效果

斜体

rst如下:

*斜体*

效果如下:

斜体

粗体

rst如下:

**粗体**

效果如下:

粗体

下标

斜杠是为了空格转义,最后显示无空格。

rst如下:

H\ :sub:`2`\ O

效果如下:

H2O

上标

rst如下:

E = mc\ :sup:`2`

效果如下:

E = mc2

参见

Hello World

根据上面的介绍,其实常用的语法不多,现在直接用下,自己感受下吧!

安装 & 初始化

常用Python安装方式,创建个文件夹,执行命令,按提示自己选择即可。

pip install Sphinx
mkdir docs
cd docs
sphinx-quickstart

根据提示输入相应参数即可,可以一路默认。

尝试编辑

编辑index.rst,只写入以下内容

用Sphinx编写技术文档
====================

使用场景
--------

生成HTML

很简单,默认支持就很好。

make html
python -m SimpleHTTPServer 9527

直接浏览器访问9527端口,就可以看到类似Python官方文档的效果。

生成PDF

麻烦些,需要依赖库,且需要简单修改下配置。

  1. 安装依赖库
pip install rst2pdf
  1. 编辑conf.py,增加或修改如下配置:
  2. 编辑Makefile,增加如下代码:

Linux下的Makefie:

Windows下的批处理:

  1. 执行生成PDF
make pdf
python -m SimpleHTTPServer 9527

参见

有关PDF的更多配置,可以阅读这个文档:http://ralsina.me/static/manual.pdf

生成Slide

Slide就是我们常说的演示文档,如:Windows下的PowerPoint(PPT);Mac下Keynote等等。这里用Sphinx生成在线的HTML5形式的Slide,操作也相对简单,也是需要依赖库和简单修改下配置。

  1. 安装依赖库
pip install hieroglyph
  1. 编辑conf.py,修改如下配置:
  2. 编辑Makefile,增加如下代码:

Linux下的Makefie:

  1. 执行生成Slides
make slides
python -m SimpleHTTPServer 9527

参见

有关Slide的更多信息,可以直接查看这个项目:https://github.com/nyergler/hieroglyph

自定义样式

直接拿来主义,直接用别人写的Trac的样式

  1. 复制样式文件到静态资源目录,比如,这里是:
cp tracsphinx.css _static/
  1. 编辑conf.py,增加或修改如下配置:
  2. 执行生成HTML
make html
python -m SimpleHTTPServer 9527

直接浏览器访问9527端口,就可以看到类似Trac的官方样式效果。

汇总到一块

可以直接看Python项目模板:https://github.com/akun/aproject/只看docs目录即可。

这里提到的几个核心文件示例如下:

  • index.rst
  • conf.py
  • Makefile
  • css

另外推荐一个服务:https://readthedocs.org/

如果你的项目研发文档用Sphinx写的,可以用来做文档的持续集成,相当方便。

这个《软件构建实践系列》就是用的这个服务。

最后

这是一篇很简单的项目推广文章,在自己的Python项目中把Sphinx用起来吧!

当然Sphinx不仅支持Python源码的Domain,而且支持C、C++、JavaScript等Domain,即使没有你所用的语言的Domain,它本身还支持写插件扩展,所以其它类型语言的项目也不妨用一下。

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.org。并且部分内容已经公布在GitHub上:https://github.com/akun/pm

SVN命令用法:程序员的场景

SVN有不少命令,其实常用的也就那么几个,可以结合下实际的使用场景,来说明下SVN的命令用法。

当然可能对很多人来说,最实用的熟悉方式,就是直接运行

svn help (?, h)

就入门了,但为了更好的记忆,有个实际场景也是个不错的选择。

注解

括号中的是该命令的缩写或别名,有的可以少打几个字母,后面也有类似描述。

“新人报道”

你刚入职一家公司,或新加入某个团队,立马参与到一个项目中,项目代号Norther,那么就得获取项目代码,开始你的项目生涯。这个时候一般你需要签出项目代码:

svn checkout (co) https://scms.ship.it/svn/norther/trunk norther

确认工作目录的SVN信息,说明已经纳入版本控制了:

cd ~/projects/norther
svn info

确认没问题了,就运行项目中的构建脚本,然后就可以熟悉代码,展开具体工作了。

当然,有的时候,有一个新项目是由你发起的,你要将初始化的项目工程放到SVN版本仓库中:

svn import souther https://scms.ship.it/svn

确认项目已经在版本仓库中了:

svn list (ls) https://scms.ship.it/svn/souther/trunk

应该就可以看到Souther项目的根目录结构了。

日常工作

当你已经逐渐融入了一个项目,可能一天的工作场景或完成某个任务的工作周期是这样的:

更新

无论是清早或下午或晚上,开始了你的一天工作,你首先会更新你的工作目录:

cd ~/projects/norther
svn update (up)

这样你就可以在最新的项目代码基础上工作了。

修改

可能你写了一个新的模块,需要纳入项目的版本控制:

svn add tools.py

可能你发现某个模块已经陈旧了,不再使用了:

svn delete (del, remove, rm) utils.py

可能你发现一个模块的命名不太合理,需要改名:

svn move (mv) model.py models.py

可能你要创建一个新的较大的模块,需要归档为目录的方式:

svn mkdir groups

可能你发现要写的模块代码布局类似于旧的模块,直接复制个代码模版:

svn copy (cp) users/tests.py groups/tests.py

当然,其实最常见的情况其实还是打开编辑器,比如Vim,修改已经存在的代码,这个就跟SVN命令无关了。

检查

忙碌的一天过去了,或者一个任务完成了,这个时候一般会将你的工作成果,也就是代码更新到版本仓库。

习惯上会先检查下修改状态:

svn status (stat, st)

看到一些SVN状态位信息,确认是修改了哪些文件,之后一般会自己code review一下代码的改动,可能有的人会习惯直接用SVN方式来查看:

svn diff (di)

然后本地运行下相关的单元测试,确认是否有问题。一般来说这个时候,没有什么特殊情况,就直接进入“提交”阶段了,然后结束一个工作日或工作周期,但难免会有些特殊情况出现。

取消修改

当你code review完后,发现有些改动不满意,你可能又会取消这些修改:

svn revert main.py

解决冲突

当你打算提交时候,习惯上一般会再次更新自己的工作目录,现在合并下别人的工作成果(如果有的话):

svn update (up)
  • 可能这个时候更新完代码,你对某个模块的代码有改动,别人也改动了同一个模块的代码,可能就会产生代码冲突。
  • 也可能有的人没这习惯,就直接提交代码,发现提交没有成功,一看,原来是别人提交的代码刚好也改动了你提交的代码,也产生了冲突。

无论哪种情况,就是代码冲突了,需要解决冲突,一般会人工确认代码合并,处理冲突的代码,是选择别人的处理,还是自己的处理,还是要额外处理,处理完毕后,执行命令,比如:

svn resolve main.py --accept working

另外,也有个resolved命令,用来删除“冲突”状态,但官方说被上面命令替换了,不推荐使用了:

svn resolved main.py

提交

最后,一切确认没问题了:code review完毕,自己觉得代码满意了;然后也合并完别人的修改并且没有冲突了;运行单元测试也通过了。那么就提交代码吧:

svn commit (ci)

在分支工作

源代码的管理和发布:以SVN为例 这篇文章中,介绍的SVN开发模式中,涉及分支的概念,一般来说会有以下3种情况:

  • 贡献新特性。也就是说,为了增加新的功能,或者对旧功能的改进等等。
  • “除虫”。就是日常说的缺陷修复。
  • 发布阶段(发布分支)->旧版本维护(旧版本维护分支)。这个概念稍微复杂,trunk研发到某个阶段,代码符合某个版本发布条件了,就会新建1个发布分支,测试没问题了,就在这个分支上进行发布;发布完成后,这个版本的维护就在这个维护分支上进行了;这个时候trunk已经进行最新版本的研发了,所以说这个分支是个旧版本维护分支。

上述说的3种分支情况,前两个分支的生命周期比较短,新特性搞定或“除虫”完毕,合并代码到trunk后就结束自己的生命周期了。

最后一种情况,生命周期相对较长,如果这个分支需要维护的版本还要支持,那么就得一直存在,直到不再维护为止。

下面说下在分支工作的实际场景,按顺序:

创建新分支

当上述3种场景发生,确定要新开个分支来写代码,先复制trunk到分支,这里以贡献新特性为例子:

svn copy (cp) https://scms.ship.it/svn/norther/trunk https://scms.ship.it/svn/norther/branches/feature1

切换到新分支

一般来说这个时候本地的工作目录是trunk,确定本地工作目录是干净的,为后续在分支工作,以及合并分支做好准备,避免可能的各种代码冲突或工作成果代码被覆盖等情况出现。

确认当前所在的SVN工作目录,比如,可能是在trunk的SVN路径:

svn info

确认工作目录干净:

svn status (st)

切换到刚才新创建的分支:

svn switch (sw) https://scms.ship.it/svn/norther/branches/feature1

确认切换后的SVN工作目录,应该就是在刚才新创建的分支的SVN路径了:

svn info

在新分支工作

类似,“日常工作”中的工作周期操作,这个时候,你就可以在新分支中进行大刀阔斧的工作了,直到分支中代码符合合并到trunk的的条件了。

合并分支到trunk

在分支中工作一段时间后,确认相关的功能代码、测试代码、文档等都提交完毕了,单元测试通过,大家code review一致认为没问题,审核通过,最后该分支的持续集成(CI)完整build通过。这个时候,就可以进行合并到trunk的操作了。

确保下面操作是在工作目录的根目录下进行

cd ~/projects/norther/

确认分支工作目录干净,没有需要提交的代码了:

svn status (st)

切换工作目录回trunk,如果由于代码变动大有冲突,就解决冲突,特别如果有目录变动很可能有目录冲突:

svn switch (sw) https://scms.ship.it/svn/norther/trunk

确认切换后的SVN工作目录是trunk:

svn info

先在本地合并分支的代码,合并过程可能会有代码冲突,解决冲突,合并会指定版本范围,一般都是分支建立时候的版本号到分支工作完毕时候最后一次提交的版本号:

svn merge -r9527:9549 https://scms.ship.it/svn/norther/branches/feature1 .

确认本地代码变更,code review一下,执行下单元测试:

svn status (st)
svn diff (di)

确认代码没问题,正式提交代码到trunk,SVN的提交日志说明下合并信息:

svn commit (ci)

删除分支

如果确认工作完毕的分支不再需要了,那就记得及时清理掉:

svn delete (del, remove, rm) https://scms.ship.it/svn/norther/branches/feature1

Ship it

在上面说的发布分支工作一段时间后,并且测试完毕,大家觉得符合发布条件了。终于可以进入到版本发布阶段的工作了。

具体故事场景可以看 源代码的管理和发布:以SVN为例 这篇文章,有对“发布分支”的介绍。

一般来说这个时候已经将trunk复制一份到了发布分支了:

svn copy (cp) https://scms.ship.it/svn/norther/trunk https://scms.ship.it/branches/1.0.x

打标签

复制最新的发布分支为标签:

svn copy (cp) https://scms.ship.it/svn/norther/branches/1.0.x https://scms.ship.it/svn/norther/tags/1.0.0

正式发布

发布又是一个比较复杂的主题,比如:能快速发布、快速回滚(包括数据回滚)、灰度发布等等,在 构建发布工具 中会详细进行介绍,这里就简单罗列下。

情况1:完整包。导出代码,然后执行打包命令,进行完整安装:

svn export https://scms.ship.it/svn/norther/tags/1.0.0 norther

情况2:补丁升级包。相对复杂,可能会综合运用下列命令,制作补丁安装升级包:

svn status (st)
svn diff (di)
svn patch

情况3:线上更新。一般不太推荐,需要注意不要泄露“.svn”,特别是旧版本的SVN,每个目录下都有“.svn”。可能会用到下列命令:

svn update (up)
svn switch (sw) https://scms.ship.it/svn/norther/tags/1.0.0

一般来说,根据实际情况,可以记录下来发布相关的操作过程。很多环节可以写脚本将原来的人工操作改成自动化操作。以后只要执行发布脚本执行一键发布就可以了。

其它场景

可能还有很多别的场景,比较零散,但也算经常用到。

code review查看代码,要知道对应代码是由谁写的,好询问了解具体代码的思路:

svn blame (praise, annotate, ann)

跟踪问题时候,会查看日志,更方便历史代码定位:

svn log

经常碰到代码锁定或很多诡异情况:

svn cleanup

编辑特定属性,比如:定义忽略规则;依赖其它SVN路径等等

svn propedit (pedit, pe) svn:ignore .
svn propedit (pedit, pe) svn:externals .

SVN客户端更新,使用新的SVN客户端了,有时候会发现本地工作目录SVN相关信息陈旧,会提示你升级:

svn upgrade

好习惯

这里顺带说下几个使用SVN的好习惯,但有的其实跟SVN联系也不算大,只是顺带提下:

  • 保持工作目录干净。或者说工作目录中的代码变更就为了完成一个任务,即一次只做一件事。完成任务后,就直接svn commit (ci)提交到版本仓库,而不用担心其它任务作出的代码变更无提交。并且,对于分支和trunk间切换更方便,而不用担心代码被覆盖或冲突的问题。
  • SVN的日志信息足够有效。足够有效的意思,是说这次提交作出的变更摘要,只要别人阅读了日志就能知道大概,如果为了深入了解变更细节才会去查看具体代码变更。
  • svn commit (ci)前先svn update (up)。可能不更新提交也没问题,但也有可能会相关代码被别人改动了,而提交不了,为了避免这种情况,先本地更新完毕,确保别人的改动不影响你对代码修改的意图。
  • svn commit (ci)前code review。code review本身就是个好习惯,提交前确认是一种更为严谨的方式,如果觉得自己code review发现不了什么问题,那么随便从身边抓个会代码的,跟别人讲解下代码变更的内容,说不定会发现你没考虑到的问题。
  • svn commit (ci)前跑单元测试。写单元测试本身也是个不错的习惯,如果项目本身已经有了完备的单元测试覆盖了,那么你对代码的修改,应该能通过单元测试,所以提交前执行一遍是否通过。如果没通过,就得看下是功能代码写的有问题,还是测试代码有问题,比如:功能需求或接口设计有变化,而测试代码没有同步更新。
  • 有代码变更及时提交。有SVN这种版本控制工具,本身就是为了记录研发过程,避免意外导致代码丢失,如果为了完成某个任务需要很长时间,代码也很久没有提交,风险太高。这个时候,一般会自己开个分支,而将代码提交到分支中,既解决代码要及时提交的问题,又解决代码提交频繁,可能造成代码不稳定影响别人的问题,因为那个分支只有你自己在工作。

最后

这些场景覆盖的SVN命令其实很有限,如果要完整的熟悉,那就svn help以及阅读下SVN的官方手册,有个系统的学习,基础才会更加牢固。

后续

另外,这里只是以程序员的场景来简单介绍SVN使用,对于系统管理员,可能有一部分职责是作为SVN版本仓库管理员,日常也会遇到的各种场景吧,后续也会简单介绍。

参考

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.org。并且部分内容已经公布在GitHub上:https://github.com/akun/pm

源代码的管理和发布:以SVN为例

前几天在微博吐槽了SVN的几个不爽的地方:.svn文件满天飞、分支管理的麻烦。不爽一般来说都是有过对比后才有如此感觉,比如:相比于Git;或者你对该事物了解不够,比如:.svn的问题,其实早在1.7版本中已经解决了。

至于分支管理,如果把控好,再加上项目本身代码质量足够高,其实也没那么麻烦。如果觉得麻烦,可能很大程度上,是项目的代码本身就很糟糕,造成管理上的麻烦,比如:要修改个特性,估计需要一段时间,为了不影响主干代码稳定,需要在分支中开发,到合并代码的时候就各种麻烦,但觉得麻烦其实很多时候是因为代码耦合度太高了。

下面有关SVN代码的管理和发布,其描述前提是团队成员往SVN贡献的代码,质量都是足够高的,并且前提是开发商业软件,会有钱、时间、质量这坑爹3角因素的影响,而不同于开源项目模式。

目录布局回顾

先简单回顾下SVN的目录布局吧。

项目在版本仓库中的目录

trunk、branches、tags,这是一个项目在版本仓库中典型的目录布局。

  • trunk:主干,如果说把一个软件项目从开始到消亡比作一个故事的话,主线情节都在这里被SVN记录着。
  • branches:分支,有很多种用法,比如:版本发布维护分支、新特性开发分支,甚至是缺陷修复分支等等。
  • tags:标签,或者叫快照,某个版本发布时候,都在这里留档。

示例如下图:

http://zhengkun.info/wp-content/uploads/2013/09/SVN_tree_example2.png

不同版本仓库

对于一个比较大的项目,可能会拆分为多个子项目,各个子项目团队,按照约定各自进行开发;或者一个公司有很多项目,每个项目团队都各自进行开发。无论哪种情况,这个时候版本仓库的建立有2种选择,公用和独立,其实对于使用SVN客户端的程序员来说没太大区别,唯一不同的是前者是公用一套SVN版本号,而后者是独立使用各自的SVN版本号的。而在这个问题上,SVN又不同于Git,Git的所谓版本号就可以认为全球都唯一,甚至全宇宙唯一?哪怕不同公司的版本仓库。

其实上面说的版本仓库的管理,理解上就可以认为是目录管理,再加上时间这个维度。下面以可能出现的三种情况作为例子,来说明SVN代码的管理和发布。

适合小团队情况

小团队,不同人的理解可能不同吧,这里就定量一下规模,索性特指2-4人水平都一样的小团队吧。在人员较少的情况下,如何来控制trunk、branches、tags这3者的关系?其实最简单的方式是大家都在trunk上贡献代码,觉得某个版本差不多了就tags一下,相当于和branches说拜拜了。

但这里其实会有几个问题:

  • 要开发某个特性,会花费挺长时间,这段时间不提交代码不合适,提交了又会影响trunk代码稳定性,怎么办?
  • 觉得差不多了是什么概念?能保证发布了某个版本测试没问题?保守点考虑,还是认为会有各种问题,需要严格测试,定量了,如果发现问题还得进行缺陷修复吧。
  • 这个时候,另一个问题出现了,发布这个版本的那段时间,团队中的1-2个人完成本次版本的开发了,又想进行下个版本的开发工作,岂不是得停滞了,因为他们也不知道当前发布的版本有没有问题。

针对上述几个问题,可以尝试下下图的模式,但需要配合几个其它实践,保证迭代质量。

http://zhengkun.info/wp-content/uploads/2013/09/SVN_small.png

上图的意思,还是2-4人的小团队,集中战力在trunk进行新版本研发+旧版本维护,只不过区别就是,到了上面说的觉得差不多了,新的大版本或旧版本的某个小版本代码稳定的时候,分出去一个发布分支,由1-2个人严格配合测试,在该分支上工作,有需要修复的缺陷,有人在trunk上修复了,可以直接把该修复merge到发布分支上。

这里约定,新版本主要进行新特性的发布;旧版本进行旧特性的问题修复。但这里有个疑问,新特性的代码和旧特性的代码都在一起,那发布旧版本问题修复版本时候,新特性的代码咋办?这里就得用到其中1个实践,叫做“特性开关”,虽然新特性的代码已经提交到trunk,但发布旧版本的问题修复版本时候,该特性不会开放出来。对于用户来说,这个还是个旧版本的缺陷修复版本。

新特性的研发,必然伴随着代码的不稳定,代码质量的下降,这个时候就得用到另外2个实践,“trunk上每次代码提交进行codereview”和“每次代码提交的持续集成”,说白了就是每次代码提交都要有充分的反馈,如果团队的codereview习惯足够好,以及项目本身有了一套成熟的持续集成的话,就能一定程度上避免这个问题,如果发现有问题,立马反馈,及时修正。

还有最后一个疑问,都在trunk中,怎么保证新特性的代码和旧特性的代码管理起来足够分离?这个就得考验团队的设计能力时候了,是否能让各个功能模块间的耦合度足够低,从而把影响降低,说白了还得程序员足够厉害。当然这里说的“特性开关”不要太多,2-4人的小团队,2个大的特性开关已经很复杂了。如果这种开关很多咋办?这个时候其实不是你们团队从技术层面或管理层面需要解决的问题了,很有可能是当前的功能必须要更多人维护开发,就是需要扩充团队了;或者,根本不需要这么多功能特性,直接砍功能吧。

下面讲一个理想模型,比如项目资金流能撑得起更多的团队人员的情况下,如何更高效的研发。

比较流行的、推荐的

直接看图,跟上图很类似,区别就是,新特性研发trunk和旧特性维护branches分开来了,并且由2个小组分别负责:

http://zhengkun.info/wp-content/uploads/2013/09/SVN_normal.png
  • 红色外框代表小组1,一开始是新特性开发小组,该特性发布后,就变成旧特性维护小组了。
  • 蓝色外框代表小组2,一开始可能是旧特性维护小组,另一个小组进行维护他们开发的特性的时候,这个小组角色就变成新特性开发小组了。
  • 当然两组是实力相当的小组,他们来回切换新特性开发和旧特性维护。
  • 并且能保障新版本研发和旧版本维护可以同时进行。

为了更好说明,直接引用一段SVN官方文档的故事描述

http://svndoc.iusesvn.com/svnbook/1.4/svn.branchmerge.commonuses.html#svn.branchmerge.commonuses.patterns

发布分支

大多数软件存在这样一个生命周期:编码、测试、发布,然后重复。这样有两个问题,第一,开发者需要在质量保证小组测试假定稳定版本时继续开发新特性,新工作在软件测试时不可以中断,第二,小组必须一直支持老的发布版本和软件;如果一个bug在最新的代码中发现,它一定也存在已发布的版本中,客户希望立刻得到错误修正而不必等到新版本发布。

这是版本控制可以做的帮助,典型的过程如下:

* 开发者提交所有的新特性到主干。 每日的修改提交到/trunk:新特性,bug修正和其他。
* 这个主干被拷贝到“发布”分支。 当小组认为软件已经做好发布的准备(如,版本1.0)然后/trunk会被拷贝到/branches/1.0。
* 项目组继续并行工作,一个小组开始对分支进行严酷的测试,同时另一个小组在/trunk继续新的工作(如,准备2.0),如果一个bug在任何一个位置被发现,错误修正需要来回运送。然而这个过程有时候也会结束,例如分支已经为发布前的最终测试“停滞”了。
* 分支已经作了标签并且发布,当测试结束,/branches/1.0作为引用快照已经拷贝到/tags/1.0.0,这个标签被打包发布给客户。
* 分支多次维护。当继续在/trunk上为版本2.0工作,bug修正继续从/trunk运送到/branches/1.0,如果积累了足够的bug修正,管理部门决定发布1.0.1版本:拷贝/branches/1.0到/tags/1.0.1,标签被打包发布。

整个过程随着软件的成熟不断重复:当2.0完成,一个新的2.0分支被创建,测试、打标签和最终发布,经过许多年,版本库结束了许多版本发布,进入了“维护”模式,许多标签代表了最终的发布版本。

你会发现其实很多开源项目就是这么干的,这种模式很通用,不仅限于使用SVN的情况下。

稳定的、干净的trunk

再更进一步,如果项目规模很大,需要参与的人员较多,这个时候代码管理需要更为严格,需要对代码提交要求严格,保持trunk足够稳定、足够干净,可能就会有很多分支出现,比如常见的特性分支,甚至是修复一个缺陷,也会开一个缺陷分支,经过严格的codereview后再merge到trunk或某个维护分支,该实践会牺牲一定的灵活性,给团队成员带来繁琐的感觉,并且迭代起来会慢,建议谨慎使用,如果要用,得有足够理由。示意图如下:

http://zhengkun.info/wp-content/uploads/2013/09/SVN_strict.png

习惯上,如果项目很大,一般都会拆分为多个子项目,各个子项目的各个团队都分别按小团队作战,来避免上述情况,从而提高迭代速度。

或者从工具使用上,可能这个时候使用的是Git,就能更好的解决类似问题,这个在以后的文章中有机会单独说下。

结束

来个箴言作为结束吧:

童子军规则,“要让离开时的营地比进入时更加干净”

类比到写代码,“让模块签入(check in)的时候比签出(check out)的时候更整洁”

再装X些,“做人也是,你离开世界的时候能让世界变得更美好,哪怕一点也行!”

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.org。并且部分内容已经公布在GitHub上:https://github.com/akun/pm

Pylint插件:忽略Django的国际化代码调用报错

NOTICE:如果更新到Django1.4+以上版本后,就不会报类似Error级别的提示了。

用Django写Web开发类型的程序,经常会用到程序的国际化操作,例如,保存如下代码为example.py:

from django.utils.translation import ugettext_lazy as _

print _('damn it!')

如果项目中用到Pylint来检查代码,执行如下:

pylint -E example.py

这个时候会显示一个Error级别的提示,如下:

No config file found, using default configuration
************* Module example
E:  3,6: _ is not callable

熟悉Django的人,一定会觉得很奇怪,怎么可能函数不能被调用,这里就不具体展开说明原因了:

  • 可以认为是Pylint不够智能
  • 也可以认为Python本身太灵活,Django中调用的国际化模块,刚好用了一些动态语言的特性

怎么办?

  • Pylint提供了灵活的配置设置,过了一遍配置,发现没有自定义设置可以忽略这个情况
  • Google之,但很多是说直接忽略那条规则,这样虽然可以达到忽略的效果,但是同样也忽略了不应该忽略的检查,比如:a = 1; a()这种语法错误
  • 看Pylint官方文档,发现Pylint扩展性还是不错的,提供自定义插件机制,-–load-plugin参数指定插件即可,那么就自己写个插件来处理这种情况吧!
  • 继续Google,发现有人写过类似插件,虽然是处理别的问题,但很类似:http://www.logilab.org/blogentry/78354

写个插件!

代码很简单,原理就是写个假的函数调用,在Pylint执行该模块的函数检查时候,直接调用了假的函数,从而绕过该报错,但又不会影响这条规则检查其它代码时候报告Error级别信息。保存如下代码为astng_django.py

from logilab.astng import MANAGER
from logilab.astng.builder import ASTNGBuilder

def hashlib_transform(module):
    if module.name == 'django.utils.translation':
        fake = ASTNGBuilder(MANAGER).string_build('''

def ugettext_lazy(value):
    return u''

def ugettext(value):
    return u''

''')
        for hashfunc in ('ugettext_lazy', 'ugettext'):
            module.locals[hashfunc] = fake.locals[hashfunc]

def register(linter):
    """called when loaded by pylint --load-plugins, register our tranformation
    function here
    """
    MANAGER.register_transformer(hashlib_transform)

这个时候执行命令(需要PYTHONPATH能找到astng_django.py)

pylint -E example.py --load-plugins=astng_django

输出如下:

No config file found, using default configuration

就没有刚才的Error级别的提示了。

保存如下代码为error.py,再验证下报错的情况是否被忽略:

fake_function = 1
fake_function()

执行命令:

pylint -E error.py --load-plugins=astng_django

输出如下:

No config file found, using default configuration
************* Module error
E:  2,0: fake_function is not callable

这样就算初步解决这个问题了,即可以不让Django程序报干扰的Error级别信息,又不会忽略的确需要报的Error级别信息。

占个坑~

照例,GitHub上先占个坑:https://github.com/akun/pylint_plugin

后续TODO:

  • 前面有说明必须指定PYTHONPATH,有点麻烦,但作为临时解决问题是够了。后续需要做个完整的Python库,然后发布到PyPI,然后作为正式的安装包,就更方便别人使用了。
  • 另外,这个插件还是相对简单,其实Django中的国际化处理有好多函数,需要后续补充。
  • 将上述的相关验证转化为对应的测试代码。

Python中的单元测试

^

上篇文章《 你好,单元测试! 》闲扯了下个人对单元测试的看法。

后续几篇  单元测试  主题的文章,打算先选几个常见的编程语言作为示例来讲解,因为最近个人的主要编程语言是Python,那就必须先以Python为例讲解最省事了;-)

举个“栗子”

http://zhengkun.info/wp-content/uploads/2013/06/lizi.jpg

偷懒,就直接拿Python官方文档中的例子来做说明好了;-)

# http://docs.python.org/2/library/unittest.html?highlight=unittest#basic-example

import random
import unittest

class TestSequenceFunctions(unittest.TestCase):

    def setUp(self):
        self.seq = range(10)

    def test_shuffle(self):
        # make sure the shuffled sequence does not lose any elements
        random.shuffle(self.seq)
        self.seq.sort()
        self.assertEqual(self.seq, range(10))

        # should raise an exception for an immutable sequence
        self.assertRaises(TypeError, random.shuffle, (1, 2, 3))

    def test_choice(self):
        element = random.choice(self.seq)
        self.assertTrue(element in self.seq)

    def test_sample(self):
        with self.assertRaises(ValueError):
            random.sample(self.seq, 20)
        for element in random.sample(self.seq, 5):
            self.assertTrue(element in self.seq)

if __name__ == '__main__':
    unittest.main()

先简单说下代码的意思,也就是:写1个测试用例(TestCase),用来测试Python自带的random模块,那1个测试用例里面包含了3个测试,分别用来测试random模块的3个函数(看Python源代码,其实在random模块中已经把一个Random类实例化了,所以从外部用法上看起来就是函数调用一样)。

再简单解释下代码中出现的几个概念,后续再详细讲解:

  • TestCase,就是测试用例,一个测试用例可以包含多个测试(为了避免混淆可以把“测试”叫“测试项”)。代码中以Python类的形式出现。
  • test_xxxx,就是测试项,根据实际的功能代码逻辑来编写对应的测试项。代码中以Python类方法的形式出现。
  • assertXXXX,就是检查点,相当于平时执行测试中的,判断测试结果是否符合测试预期结果的代码模拟。
  • setUp,就是执行测试项前的准备工作,比如:可以做一些初始化工作,这里就是初始化一个Python列表。另外,和setUp对应的还有个tearDown,后面会讲到。作为一对兄弟,相当于是平时执行测试中的2种行为的代码模拟,分别是:准备测试执行所需要的环境,以及销毁测试过程中产生的垃圾。

几个概念

其实这里讲解的就是Python标准库,unittest模块,又叫PyUnit。类似其它编程语言,也都有对应的所谓XUnit,X可以替换为Java等其它编程语言等。

这里只是简单讲解下unittest模块中的几个概念,当然其他XUnit也会有类似概念。

test fixture

貌似平时习惯直接就叫fixture了,如果非得翻译个中文名称,叫“装置”不知道合不合适,也就是测试需要的装备。理解上可以直接对应到上面提到过的setUp和tearDown。所谓fixture就是:比如,程序运行的前提需要数据库,还得准备测试用的数据,常见的那些对数据库的操作程序就会如此;又比如,程序运行的前提得访问某个网页,常见的那些爬虫程序就会如此。类似这些,得需要准备好这些fixtures,而这个装置能有所谓清理和还原的功效(tearDown),这样不至于各个测试执行的时候有环境污染造成各种诡异情况。

test case

这个最直白,也听的最多,叫测试用例。理解上可以直接对应到上面提到过的TestCase这个类。对于测试用例来说,就是针对功能代码,模拟一些输入,来验证输出是否符合预期。

test suite

测试套件,也好理解,就是包含了一堆test case的集合。使用上可以根据具体场景来归类各个test case吧,比如:根据业务逻辑分(模块A、模块B);根据测试逻辑分(全功能测试、冒烟测试)。当然,测试套件也可以包含一堆其它测试套件。

test runner

跑测试的家伙,你把各个测试丢给他,他去执行,然后把测试结果形成一份报告让你看。

关系

画个示例图,应该可以更好理解这除了test runner外的几个概念的关系吧:

http://zhengkun.info/wp-content/uploads/2013/06/concept_relationship.png

程序执行

http://zhengkun.info/wp-content/uploads/2013/06/order.png

上面这个图,就是一个TestCase执行测试代码的时候,程序执行的过程吧,想要了解更直接些,直接运行下面这个程序,看下输出信息应该就明白了。

import unittest

class ExampleOrderTestCase(unittest.TestCase):

    def setUp(self):
        print
        print 'I am setUp'

    def tearDown(self):
        print 'I am tearDown'

    def test_do_something(self):
        print 'I am test_do_something'

    def test_do_something_else(self):
        print 'I am test_do_something_else'

if __name__ == '__main__':
    unittest.main(testRunner=unittest.TextTestRunner(verbosity=2))  # Python 2.6
    #unittest.main(verbosity=2)  # Python 2.7

控制台输入如下:

test_do_something (__main__.ExampleOrderTestCase) ...
I am setUp
I am test_do_something
I am tearDown
ok
test_do_something_else (__main__.ExampleOrderTestCase) ...
I am setUp
I am test_do_something_else
I am tearDown
ok

TestCase

一般来说,日常用Python写单元测试代码,最多的还是跟TestCase打交道。而搭建针对具体项目的测试框架时候,会用到的较多是TestSuite、TestResult、TestLoader这些,一旦项目中的测试框架搭建成体系了,很少会打交道。所以,先单独讲下大众化点的TestCase。

setUp()

执行某条测试前需要准备的工作,比如:某个文件或目录必须存在、数据库需要初始化好、网络服务要准备好、访问的URL需要登录授权完毕等等。

每次调用测试前,都会执行这个方法。如果你运行过上面的程序就应该了解。

顺便讲一下2个概念:测试错误(Error)和测试失败(Failure)。

  • 测试错误,可以简单理解成测试代码执行时候报错了,比如:测试代码中print a,而a没有进行变量声明。
  • 测试失败,可以简单理解成测试代码执行正常,但没有得到预期的测试结果,比如:测试代码中调用功能代码add(1, 2),但返回结果不是3。
  • 另外,从Python 2.7开始支持了skip特性,也可以理解为测试忽略(Ignore),比如:某个测试只想在Windows下才运行,这样在Linux下就会被跳过,也就是忽略。

好了,现在可以讲了,如果代码在这个阶段出错,都会认为是测试错误(Error),比如:

import unittest

class SetUpErrorTestCase(unittest.TestCase):

    def setUp(self):
        self.assertEqual(1, 2)

    def test_one(self):
        self.assertEqual(1, 2)

    def test_two(self):
        self.assertEqual(2, 1)

if __name__ == '__main__':
    unittest.main()

执行python test_set_up_error.py输出:

EE
======================================================================
ERROR: test_one (__main__.SetUpErrorTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "unittest/test_set_up_error.py", line 6, in setUp
    self.assertEqual(1, 2)
AssertionError: 1 != 2

======================================================================
ERROR: test_two (__main__.SetUpErrorTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "unittest/test_set_up_error.py", line 6, in setUp
    self.assertEqual(1, 2)
AssertionError: 1 != 2

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (errors=2)

结果是2个errors,可以将代码中的setUp的assert修改正确了,再次执行试下,会发现结果是2个failures

tearDown()

执行某条测试完毕后需要销毁的工作,比如:删除测试生成的文件或目录、销毁测试用的数据库等等。

每次调用测试后,都会执行这个方法,即使调用的测试错误(Error)也会调用,比如:

import unittest

class TearDownAlwaysTestCase(unittest.TestCase):

    def tearDown(self):
        print
        print 'I am tearDown'

    def test_one(self):
        self.assertEqual(1, 1)
        print not_defined

    def test_two(self):
        self.assertEqual(2, 2)

if __name__ == '__main__':
    unittest.main()

执行python test_tear_down_always.py -v输出:

test_one (__main__.TearDownAlwaysTestCase) ... ERROR

I am tearDown
test_two (__main__.TearDownAlwaysTestCase) ...
I am tearDown
ok

======================================================================
ERROR: test_one (__main__.TearDownAlwaysTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "unittest/test_tear_down_always.py", line 11, in test_one
    print not_defined
NameError: global name 'not_defined' is not defined

----------------------------------------------------------------------
Ran 2 tests in 0.006s

FAILED (errors=1)

这样设计也是为了不让某个测试的错误,影响到下个要执行的测试,所以必须要执行到清理。

如果setUp就测试错误(Error)了,那tearDown()会不会执行呢?各位可以自己写代码验证下;-)

另外,跟setUp类似,如果代码在这个阶段出错,也都会认为是测试错误(Error)。

assertXXXX()

XXXX代码Equal、NotEqual等等一堆协助单元测试的判断方法,太多了直接看官方文档最直接了。问题是这么多不经常用难免记不住,所以平时基本上就记了:

  • assertEqual
  • assertNotEqual
  • assertTrue
  • assertFalse
  • assertRaises

因为大多数都可以根据这些转化出来,当然,如果记住最好了,可以帮你一定程度上简化代码,以及增加代码的可读性。比如:要明确判别一个正则输出是否符合预期,用assertRegexpMatches,一看就知道是验证正则表达式的,就比单纯的assertEqual或assertTrue的可读性强。

当然,根据自己项目中实际情况,完全可以基于上述组合,封装出更具项目中的语义表达,提高下代码的可读性,比如:下几篇文章会讲到的Django中的单元测试框架,就封装了不少适合Web开发中的assertXXXX,比如:判断是否URL跳转等。

另外,需要说明的是几个failXXXX的判断方法、assertEquals、assert_,已经不推荐使用了。

搭建自己项目中的单元测试框架

这篇文章就先引出这个主题,暂时不详细展开,后续几篇文章逐渐来展开。

下面几个也会用到,但对于一个项目,已经搭建起来了比较完善的测试框架后,这些就不会经常用到或去改动了。组合使用下面几个,就可以根据各自项目中的实际情况,来搭建一个基本的单元测试框架,后来者基于这个框架,按照约定来填充单元测试代码就可以了。

TestSuite

上面也提到了,TestSuite可以认为是一堆TestCase根据需要打个包,实际运行测试还是以TestCase为单位的。看官方文档,可以知道TestSuite有两个常用的方法,addTest和addTests,addTests可以认为是循环调用了多次addTest。这里add的Test可以是TestCase,也可以是TestSuite,反正是一个套一个,大鱼吃小鱼的关系。

几个实例,可以修改需要执行的不同suite自己执行下试试:

#-*- encoding: UTF-8 -*-

import unittest

class ExampleTestCase(unittest.TestCase):

    def test_do_somthing(self):
        self.assertEqual(1, 1)

    def test_do_somthing_else(self):
        self.assertEqual(1, 1)

class AnoterExampleTestCase(unittest.TestCase):

    def test_do_somthing(self):
        self.assertEqual(1, 1)

    def test_do_somthing_else(self):
        self.assertEqual(1, 1)

def suite_use_make_suite():
    """想把TestCase下的所有测试加到TestSuite的时候可以这样用

    """

    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(ExampleTestCase))
    return suite

def suite_add_one_test():
    """想把TestCase下的某个测试加到TestSuite的时候可以这样用

    """

    suite = unittest.TestSuite()
    suite.addTest(ExampleTestCase('test_do_somthing'))
    return suite

def suite_use_test_loader():
    """想用TestLoader方式把测试加到TestSuite的死后可以这样用

    """

    test_cases = (ExampleTestCase, AnoterExampleTestCase)
    suite = unittest.TestSuite()
    for test_case in test_cases:
        tests = unittest.defaultTestLoader.loadTestsFromTestCase(test_case)
        suite.addTests(tests)
    return suite

if __name__ == '__main__':
    unittest.main(defaultTest='suite_use_test_loader')

TestLoader

可以看到上面最后一个例子,有用到TestLoader这个类,现在简单介绍下。根据刚才的例子,可以把TestLoader简单理解成辅助TestSuite的工具,用来收集符合要求的测试,或者可以认为是一个可以批量产生TestCase的工具。

看官方文档提供了很多方法,用于适应不同的场景,大多数都是类似loadTestsFromXXXX这种方法。

默认有个实例化完毕的可以直接拿来用,就是unittest.defaultTestLoader,上面示例代码中也有体现。如果你觉得默认不满足实际使用,那么就自己写个TestLoader也可以。

另外,还有TestResult和TextTestRunner这两个很有用的东西,可以在后续介绍Django中的单元测试中来重点说明,顺便也可以简单阅读下Django的单元测试框架代码,了解下还是有好处的。如果以后在项目中,需要自定义自己特殊需求的单元测试框架的时候还是有点参考意义的。

doctest

这里简单提下,Python中还自带doctest这种形式的单元测试,就是直接把测试写在文档注释。其中一个优点是,看到注释就知道这个模块、函数、类是怎么个用法了;而其中一个缺点是,测试代码的组织上很难模块化。这里就看个简单示例吧:

def show_me_the_money():
    """
    >>> print show_me_the_money()
    $
    """

    return '$'

if __name__ == '__main__':
    import doctest
    doctest.testmod()

执行python test_doctest.py -v输出:

Trying:
    print show_me_the_money()
Expecting:
    $
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.show_me_the_money
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

$

如何来体会Python中的单元测试,直接在自己的项目中写段单元测试代码吧, show me the code 最实在了。所谓实践就得,Think -> Do -> Done -> Think

  • Think:就是得有这个意识或者说想法吧,没有意识的话,一切无从谈起。
  • Do:在自己参与的项目中,先开始尝试着写上一段单元测试代码吧。比如:修复缺陷的时候,增加新特性的时候等等。
  • Done:成为一种习惯,最后就跟呼吸一样,如果停止,你会觉得难受。
  • Think:继续Think,实践过后,每个人一定会有自己的感悟和理解。作为一个思考者、改良者、传道者,分享出来你的看法和经验吧。

后续

这里只是很简单地介绍了下Python中的单元测试,更详细的其实还是直接把官方手册相关部分完整的读一遍最实在了,当然希望这篇文章不是官方手册的重复就好。

这里讲的示例,估计实际项目中用起来,也就能应付个基本的加减乘除那种业务逻辑的场景。实际的项目,根据不同类型的开发项目,会有各种需要模拟的测试场景,这个时候一般需要借助更高级抽象的单元测试框架、模块,比如:

  • 可能你自己的项目中已经积累了适合你项目的单元测试类库,这样就挺好。
  • 还有各种成熟的各种开源开发库,比如:Python的Web开发框架Django,它里面就提供了适合Web开发场景的单元测试各种类库。
  • 还有需要模拟各种情况的类库,比如:网络请求、数据库存储、读写文件等等,Python中就提供了不少好的模拟的库(可以Google下Python Mock,官方文档给出的这个资源链接也不错:http://pycheesecake.org/wiki/PythonTestingToolsTaxonomy)。

接下去打算再简单介绍下Django中的单元测试,算是Web开发类型的场景吧,当然还是Python,有兴趣的话,还可以看下Django源代码中有关单元测试的部分,相信会有更大的收获吧。如果有别的开发类型的场景,各位也可以分享出来,大家一起开开眼界。

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.org。并且部分内容已经公布在GitHub上:https://github.com/akun/pm

“用户”是谁?

^

  • 客户就是上帝
  • 以用户为中心设计
  • 用户第一
  • 还有各种补充,你一定想的比我多

好话都会说,貌似也都知道,但不知道有多少人口中这么说,心中还骂你老母、七大姨八大姑九大舅,你二大爷的。其实这里想说的“用户”跟上面提到的没太多关系,想说说广义上的“用户”。

第0个笑话

  • 苦逼B:S啊,你写的功能模块有没有啥文档可以看看啊,感觉有点复杂啊。。
  • 序员S:B啊,没有啊,直接看代码吧!
  • 苦逼B:好吧,那你直接跟我讲讲吧。。
  • 序员S:~!@#$%^&*()_+

上述过程在业界俗称“口交”。几个月后..

  • 序员B:FVCK,谁写的功能模块啊,连个文档说明都没有!
  • 序员A:本人已死,有事烧纸。。

此时已不能“交”之。。

这种场景太常见:想让我写文档,没门;你没文档,你妹!

第1个笑话

  • 小卷的释迦摩尼:耶稣啊,听说产品UI原型图出来了?
  • 大卷的耶稣:是啊,你看就是这个!
  • 小卷都立起来的释迦摩尼:你妹!这丫的是小鸡啄米图吧!
  • 大卷如飘柔般的耶稣:没时间,凑活着用吧,好歹能有个鸟用了。。

没第三个笑话了

所谓没有,其实是想不出来了。

回到刚才说的话题,“用户”是谁?在这里,反正不是上帝,上帝啥都能干,还要你服务?类似“有*中*国*特*色*的*社*会*主*义”深刻的定义写不出来,直接摘录一段书里描述来引出说明吧:

注解

软件系统所影响的人群并不仅限于使用它的人。软件系统并不仅仅是被使用:它会被构建并测试,他需要被运维,它还可能需要修复,它经常会改善,当然它会被支付。上述每种活动都涉及很多(可能是大量)用户以外的人。每种人群都有他们自己的需求、兴趣和需要,需要软件系统来满足。 ———— 《软件系统构架》 P14

先不管书中原来想表达的意思,毕竟书中有上下文,表达的意思会有所不同。类比软件系统,可能这个软件系统是一个软件的产品,它将“用户”这一群体广义化,整个软件生命周期可能的参与者都是它可能的服务对象,就是这里想说的“用户”,同时也是书中想表达的“利益相关者”。

  • 最直接的,软件做出来给人用,使用它的人必然是“用户”。
  • 但你也知道,它是软件,会有很多问题,测试人员需要定期测试,来发现潜在问题。
  • 你访问的网站,其背后是大量服务器,而软件又运行在上面,运维人员需要日常运维,来保障服务可用。
  • 同样,它是软件,有问题就需要有人来解决,好的软件都是维护出来的,研发人员需要随时维护、开发,来改善软件。
  • 当然,钱是大家最关心的,这么好的软件,哪位大爷来给点钱烧烧呗,不然上述几个角色都不好玩了,这个角度看,有钱就是爷还是对的。

其实说到这,也就大概能说明白“用户”是谁了,也许有的人也知道如何更好的“用户就是XX”、“以用户为中心XX”、“用户第?”了。所以,以后:

  • 你平时请教别人问题时,准备好问题的上下文,说清楚你想干什么,让帮助你的人更好帮助你,从而心情愉快。如何提问可以看下 提问的智慧
  • 团队协作一起做事,你完成某个工序后,交给下个环节的人的交付物是否有价值,能让合作者觉得你是个靠谱的人,你很专业。
  • 你设计、开发、维护功能模块的时候,有没有想过你的下任程序员对你的代码第一反应是FVCK;或者再夸张点,如果下任程序员是个十足的疯子,他可能心血来潮,提着菜刀,大半夜来见你,你还敢不敢这么写代码。
  • 总之,让测试你软件的人,查看你程序输出的日志的人,维护你程序的人,阅读你文档的人,部署你软件的人,运维你系统的人,帮你做技术支持的人,是否都能心情愉快了。
  • 干活的大家心情愉快了,那么所谓的用户也好,客户也好,掏钱的大爷也好,心情不愉快那还是很有难度的。从这一角度来看“用户第一”真的有这么重要吗?

$

就以这个链接作为结束吧,对任何人都很有用的“5W1H”,只不过这里是软件开发的场景。

http://wiki.woodpecker.org.cn/moin/5W1H

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.org。并且部分内容已经公布在GitHub上:https://github.com/akun/pm

你好,单元测试!

^

平时面试程序员,如果想起来,经常会问对方2个问题:

  • 平时写代码会进行单元测试不?能聊的话,就再跟对方交流下细节。
  • 你们公司或所在的团队进行研发过程中有单元测试不?然后也是能聊的话,就再跟对方交流下细节。

其实这2个问题主要出于2个目的:

  • 个人习惯,喜欢收集统计下市面上各个IT公司的研发阶段的单元测试情况。
  • 如果面试者有单元测试习惯,当然有总比没有好嘛。

回想一下

相信有一定软件研发经历的程序员,对于以下场景或言论应该经常碰到:

  • 坑爹了,还有一周就要交付版本了,还有好多功能还没做完呢,做完的还有好多bug呢。。写单元测试?开玩笑吧 – 一个苦逼加班的程序员。
  • 擦嘞,怎么这个bug又出现了。。 – Mr.救火员经常说的话。
  • 纠结啊,代码好乱,好想重构,但万一该出问题了咋办。。算了就这样堆代码吧 – 第n个维护该代码的程序员的心理活动。
  • 你妹啊,你们开发自己不测试吗,这么多问题根本没法用嘛 – 一个很爷们的测试姑娘发怒了。
  • 你说说,为啥发布版本周期越来越长,bug反而还越来越多了呢?! – 某个高层管理者的疑惑。
  • 还有更多日常用语、抱怨、吐槽,欢迎补充。。

当然,导致这些吐槽的原因必然不是没进行单元测试导致的,这里只是为了说明,如果有单元测试的话,对于上面的场景在一定程度上能有所避免。

你怎么看?

不知道各位程序员是如何看待单元测试这个问题的?经常会有类似说法吧:

  • 我太忙了!
  • 我没时间啊!类似上面说法。
  • 我认为功能代码更重要。
  • 测试代码软件中实际不会跑。
  • 我写代码时候很仔细,边写边手工执行下测试。这其实也不错,这个执行过程其实从广义上讲,也是单元测试,但是可重复程度较低而已(比如哪天我偷懒,即时那个程序员不偷懒,但换了个程序员维护,你能确保第二个程序员不偷懒?好,即使第二个程序员不偷懒,那么第n个程序员呢?)

其实,每个程序员对于单元测试都会有自己的看法吧。就我的观点而言,单元测试是很必要,不然也不会闲的写这篇文章吧。

不是什么

说是什么之前,先说单元测试不是什么吧:

  • NOT 万能药
  • NOT 老鼠药

一种观点认为,这是个很牛X的东西,有了它,软件质量必然会好。这就是很多人觉得有了单元测试保障以后,就万事大吉了,这个想法很危险,说到底任何一种实践仅仅只是程序员工具箱中的一种工具而已,即使是所谓万能钥匙,它也仅仅能用来开锁、开门不是吗(如果你非得想出拿钥匙来抠耳屎,好吧,这也算一种功能)。再好的实践,再厉害的工具,也能让一个不合格的程序员用烂了。

另一种保守的观点认为,这个东西不好,为什么不好?无非就浪费时间,增加工作负担,很繁琐这几种理由。但的确是这样吗?很遗憾,是的,不过得换种说法:会花费原来你没花费的时间,提升你的工作质量,是否繁琐是一个习惯成自然的问题。说白了,这里其实是一个熟练度的问题。

是什么

再来说下单元测试是什么吧:

  • 润滑剂
  • 催化剂

好吧,我承认为了对应上面的“不是什么”故意类比出2个是什么,其实可以认为是一类东西,也就是对于保障软件质量来说,单元测试是让软件实际的功能代码能,更正确的运作(润滑剂),更好的进化(催化剂)。也就是更好的校验你编写代码的输入输出预期,重构改进代码时候能更大胆的对现有代码动刀子。

单元测试代码

需要指出的是,这里的单元测试是泛指,也就是至少你写完一段代码或改完一段代码,这段代码运行的主路径或代码被修改的地方要运行一遍,也就是传统的跑一遍程序。当然这个是必须的,因为你得确认程序是按你预期的输入和输出来执行的。

但这种形式的单元测试有个缺点就是可重复性太低,这里“重复性”的意思就是,任何人想任何时间可以随时跑一遍程序,等于跑这种形式的单元测试,太依赖于人,谁都有想偷懒的时候对吧。

也就是:

  • 人工跑一边函数、模块、功能,也是单元测试。
  • 但人总会偷懒(我很忙、我没时间、我病了、我状态不好、必杀绝招:我宣布这坨代码不归我管了。。),造成重复性太低。

所以更理想的单元测试方式,还是颤抖吧,把你的测试过程写成代码吧,交给机器去跑单元测试。也许你人工跑一边程序只要1秒钟,但写测试代码可能耗费了你10分钟,貌似是600倍的耗费,但你写的代码的生命周期值得拥有这600倍的耗费,代码可能被频繁改动,每改动一次,都应该测试一次(估计很多程序员都是以自己的人品保证,自己的修改是不会有问题的,从而忽略了这一次测试吧),并且可能维护代码的不是你,这么看,600倍其实不多,对吧。

功能代码 + 测试代码

这里的测试代码特指单元测试代码。

  • 互相验证
    • 其实对于功能代码和测试代码而言,两者是一个互相验证的关系,也就是“好基友,一辈子+一被子”的关系。
    • 经常有人会问,测试代码不也是代码嘛,那谁来测试啊,答案就是功能代码,不至于出现“测试测试代码”之一说法吧,然后无限递归下去吧。这个疑问的担心是,代码是人写的,难免会出问题,测试代码也是代码,所以自然也难免出问题。
    • 其实这个担心是对的,消除这个担心,一个是借助成熟的单元测试框架,减少各种细枝末节的考虑,只关注测试验证,必然能减少很多犯错,但这只能减少,想要规避是不可能的,但可以事后补救,比如测试代码的错误,导致不应该测试通过的,竟然测试通过了,那么这个时候功能代码必然也出错,这个时候不管有没有发现,就必然是产生问题了,如果被发现了,那么就知错就改呗,找到绕过测试代码的地方,看看怎么修改下测试代码逻辑,是否能测试到这种情况。
  • 先写?后写?
    • 有人经常的疑惑就是,我到底是先写测试代码呢?还是写完功能代码再写测试代码呢?这个不好说,感觉看个人习惯吧,我的习惯一般都会是偏向后写,因为很多时候在功能代码没出来前,有些测试代码的测试用例设计很难。
    • 当然一些通用、普遍或者业务逻辑简单的场景写还是可以尝试下所谓“测试先行”的做法的。
    • 这里说的前提,貌似就是由程序员自己负责来写对应的测试代码了。可能有的公司会区分出,开发工程师和测试开发工程师,前者更偏重功能实现代码为主,测试代码为辅;后者更偏重测试代码为主。这个时候,先后也许不那么重要。
  • 度的问题
    • 还有人的疑惑就是,到底写多少测试代码才算到头啊?我的想法是,别想了,永远到不了头的。合理的做法是够用就行。
    • 比如用户注册,最起码测试下注册成功的情况吧,然后测试失败的情况吧,当然失败情况一堆,咋办?挑个主流的失败场景呗。
    • 如果那些边边角角测试不到咋办?别急着一口气吃撑胖子,慢慢补呗,毕竟你主要任务还是实现那个注册功能,测试也会服务于这一点的。但如果你非得一天把罗马城建完,那我也无话可说,有钱、有时间的人就是牛X。
    • 其实我这说了也白说,度的问题,往往是自己慢慢体验的。

测试代码只是零散的测试片段,要把这些测试集合起来,必然还是要配合测试框架、工具自动化之类的。还是那句话,机器不会偷懒。

场景

说了这么多,来说个简单的单元测试应用场景吧:

  1. 天哪!我写的代码竟然有一个bug!
  2. 吭哧吭哧,调式了半天,终于找到了触发条件,一个很诡异的条件。
  3. 咚咚!打断一下,切换到传统场景,这个时候一般就继续吭哧吭哧修复功能代码了,然后提交修复,perfect!感觉很有成就感。
  4. 叮叮!忽略刚才的打断,goto到2。这么诡异的条件,太容易被忽略了,还是加段测试代码保险一下吧,免得别人修改代码时候也碰到这种坑爹的情况。
  5. 吭哧吭哧写了个单元测试代码,来模拟刚才的条件触发,果然测试不通过!
  6. 吭哧吭哧修复完功能代码。
  7. 执行单元测试模拟条件触发,测试通过,看来修复还是有效的。

其实这个好处就是能以后避免出现这种条件触发的bug被再次引入,至少有了个保障。

但是:

  • 以后不再触发该bug!
  • 了吗?不一定噢,说不定有个更诡异的条件也能触发该bug呢。。
  • 那为什么还要用?其实前面也说了,至少封堵了一个诡异条件了,如果又发现其它诡异条件,那么继续goto到1,周而复始。

示例

实际点,用代码说话才是王道。随便找几个开源项目的代码提交,来说明下单元测试的是怎么做的吧:

其实这里体现了一个软件配置管理(SCM)的深刻理解,就是: 一切开发行为,统一在版本仓库中进行自动记录。

也就是传统的手工跑一边功能代码的单元测试这个测试行为没有被记录在版本仓库中,而单元测试代码则很好的帮助记录了,这里2个示例更好的还有对应文档的更新。

这仅仅是开始

单元测试其实只是众多环节的一小部分,或者说是一个起点吧,按顺序演变:

  • 测试框架。有了成熟的框架,必然事半功倍。
  • 集成测试。其实也是借助各种成熟工具,来帮你做代码层面的集成测试吧。
  • 自动测试。机器帮你跑测试了,自然就自动了。
  • 持续构建。每次提交代码构建也好,每日构建也好,跑一遍单元测试一般都是很重要的一个步骤吧。
  • 持续发布/交付。前面基础打得好,自动化程度足够高,这种层面的持续就不是梦想了。
  • 高效高质量迭代。注意不仅仅是高效,而且是高质量。
  • 质量可靠的软件、服务、项目、产品。其实这个才是我们的目的不是吗?单元测试只不过是达到我们目的的工具箱中的众多工具之一。

$

结束语就用这句话吧: one test a day, keep bugs away..

当然这必然是夸张了,而且有些反了,有时候经常是来了个bug,才多了个test的;-)

后续

这篇文章目的,只是希望大家能对单元测试有个感性的认识,也就是:

  • 原来不知道单元测试的,能记住单元测试这个名词。
  • 原来听过的,能认可单元测试这种形式。
  • 认可了的,能产生去实践单元测试的欲望。
  • 有了欲望的,能尝试去实践一把。
  • 已经在实践的,那么就把你的实践经验、教训、总结或吐槽各种分享出来。

后续将从实际操作层面来讲解,比如以Python为例、以JavaScript为例等等。

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.org。并且部分内容已经公布在GitHub上:https://github.com/akun/pm

Marty Cagan谈产品

纯转载:

【七印部落】Marty Cagan谈产品之两周理论 第一集

http://v.youku.com/v_show/id_XMzI3NDc3NTA0.html

【七印部落】MartyCagan谈产品系列之基本产品 第二集

http://v.youku.com/v_show/id_XMzI3NDc3NzM2.html

【七印部落】MartyCagan谈产品系列之产品团队 第三集

http://v.youku.com/v_show/id_XMzA4MTU1NzI0.html

【七印部落】MartyCagan谈产品系列之用户测试 第四集

http://v.youku.com/v_show/id_XMzEyMzI0MzMy.html

【七印部落】MartyCagan谈产品系列之独门秘笈 第五集

http://v.youku.com/v_show/id_XMzI3NDc4NDI0.html

【七印部落】MartyCagan谈产品系列之产品管理 第六集

http://v.youku.com/v_show/id_XMzI3NDc4NjIw.html