测试不是问题,问题是怎么测试。

## 单元测试

我认为单元测试已经是无可争议的最佳开发实践之一。但是很多人并不同意这个观点。他们的说法无非是:写测试需要花很多时间,需求又经常变动,一但变动,一大片测试就作废了。这样又浪费时间,又降低效率。

但现实情况是:没有人不测试代码的。哪怕是最牛的开发者,也需要对自己写的代码进行测试。这一点可以去看《Coders at Work》。虽然有一些“牛人”测试的方法很原始——比如使用 `printf` 来查看运行结果。其实,从现代软件组件的角度来看,就是用 Log。而一但软件变得复杂,涉及十几个,甚至几十个模块相互依赖的时候,看 Log 显然又低效,又费力。很多时候需要添加一些 Log,分析一下是不是这一部分的问题,然后注释掉,再在别的地方再加一些 Log。如此反复。如果是动态语言还好,如果是需要编译的语言(比如做iOS开发),那么这些小的修改最后浪费在编译上的时间也是很多的。

当然,基于现在强大的 IDE 开发工具,还有一种非常方便的调试工具,就是断点调试。使用 Visual Studio 一路杀上来的程序员最喜欢这种方法了。只要在自己怀疑有问题的地方加一个断点,在这点处的上下文就都能看到了。还能通过单步执行来看到实际的执行路径,看看是不是和自己设计时候想的一样。但是,断点调试的主要功能是在已经知道某一部分有问题的情况下,查找和修复问题。关于这一点,其实可以引用质量管理体系中的两个概念:QC 和 QA。所谓 QC,就是质量控制(Quality Control)。通常的方法是对产品进行抽样,检查合格率。然后再通过合格率来改进生产流程。而 QA,叫作质量保证(Quality Assurance),方法是监控生产过程中的每一个环节,确保每一个步骤都是在“误差允许范围内”。当然并不是说 QA 就一定要优于 QC,但这里的比较已经超过了我的能力范围。感兴趣的朋友可以自己去查看网上的各种文章。

自动化的单元测试,就相当于实现了软件开发中的 QA(感觉业内现在常说的 QA,其实做的是 QC 的工作,或者叫 Acceptance,也就是验收)。但并不是每个人都接受自动化测试这个想法。原因之一是上面说的“嫌麻烦”。而还有一派则认为:单元测试并不能完全消除 Bug,所以不值得投入时间和精力去做。持有后一种观点的朋友,很可能是被一些公司的广告给骗了。单元测试本来就不是用来发现 Bug 的工具,如果想发现 Bug,那最好的工具应该是代码的静态分析工具。而单元测试,按照我的理解,应该是使用一组代码来描述系统的行为。具体来说,进行单元测试至少可以获得下面这几个好处:

### 1. 避免过度设计

一但开始进行测试,开发的目标就会改变。原来比较倾向于“完美设计”的方案,就会被“通过测试”代替。“完美设计”会让人在开发的时候“想入非非”,常常为“不存在”的未来需求过度设计。相信每个程序员,或者架构师都遇到过这样的问题:现在这个设计虽然可以使用,可如果用户量达到 xx 万,就不能用了。虽然这种设计并不总是杞人忧天,可如果你 PV 还不到 100 每天,就使用 Google 的架构,那显然是太复杂了。而使用单元测试之后,开发的目的就无比的明确:通过所有的测试。你当然可以通过“完美设计”来更优雅地通过所有的测试。但这种“完美设计”只局限在这个局部,影响的范围有限。

### 2. 大胆重构

单元测试的另一个好处就是:定义了被测系统的行为。这个时候,如果你对这个模块/函数进行重构,就不会担心会引入破坏性的改变了。而在没有测试的时候,可能出现的情况就是:我只改动了两行代码,怎么整个程序都不能用了。如果没有单元测试,结果就是一个模块可能变的越来越大,越来越复杂,越来越不可维护。没有人敢去修改那些没有测试保护的旧代码,因为至少它现在还能用,如果改了,不知道还能不能用。相反的,如果有测试保护,那么修改和重构就是有保障的。也许有人会说:就算有测试,也一样可能引入原来没有被测试到的问题,结果还是破坏了代码。这当然是可能发生的情况。但因为有测试,所以那些通过了的测试会告诉你:你的修改没有破坏什么,这就在很大程度上减少了查错的范围。要知道:Bug 是加班之源!

### 3. 简化集成

虽然单元测试是“自扫门前雪”的测试,但同样对团队合作有很大的促进作用。在没有测试的时候,一个错误的发生通常伴随的是各种甩锅:每个人都会认为自己的代码没有问题,是别的人错误导致的系统错误。为什么会这样呢?因为程序代码中大部分是逻辑判断。哪怕是经历的不完整的逻辑推理,也会给做推理的人一种假象:我的逻辑结果是这样的,所以不会有错。同样,因为没有测试,两个相互交互的模块并不能直观的确认是发生了什么问题,只能大家坐在一起,分析调用者给被调用者发送了什么参数,被调用者应该怎么响应。而这种合作通常也是伴随着争吵和推责的。

如果两个模块都有测试呢?那么至少在每个独立的模块内部,行为已经被详细描述过了,如果出了问题,那最可能的就是在模块之间的界面处有问题。这就比原来更容易确定错误的位置和类型。也就使得模块间的集成成本有所下降。

### 4. “吃狗粮”

有的时候,我们写的代码(模块、函数)其实是给别人提供的服务。这里有一个常用的说话就是:要吃自己的狗粮。如果在开发的时候同时写测试的话,那么就相当于在测试中使用自己开发的函数,那么就更容易发现使用过程中的不方便的情况。

但我也想强调一个经常被说,但可能没什么人当回事的问题:测试代码也是代码,不要写低质量的测试代码。我相信有人和我一样,会犯懒,对于类似的测试代码,直接复制过来,简单改两下就用。这当然不是不可以,但改的时候也应该把这段代码仔细改全。我之前经常犯的一个错误就是“张冠李戴”,也就是只改了一下函数名,别的都没怎么细看,就去运行测试了。这样的结果当然就是得改很多次,才能能测试通过。

我在 《The Art of Unit Testing》这本书里,学到了一个很实用的技巧:在函数名里用下划线分成三段:被测的函数名\_输入条件\_返回值或后果。通过这样的命名,一个测试的功能就很显然了。以后在全部折叠的时候,也能知道一个函数是在测试什么东西。非常的方便。

## 测试驱动开发/设计

把单元测试用到“极致”的,可能就是测试驱动开发/设计了。虽然我很推崇这种开发模式,但实际使用的时候也还是不能完全实现:先写测试再写代码。至少我会先写一个骨架,把需要的依赖都摆放好,再开始写测试。酷壳网的陈皓曾经提到一个开发流程(他是用来反驳测试驱动开发的),就是:通常是在脑子里进行一些构思和设计,然后开始实现,并在实现的过程中进行优化。我想大部分和程序员都在使用这个开发的流程(其实我也是)。不过,我认为这个只是习惯问题。就像我前面说的:测试代码也是代码。我们通过写测试代码,对要写的代码的行为进行描述,来确定下面要写的代码的目标行为和开发边界。我在使用过一段时间之后,发现一但基本结构完成之后,实施测试驱动开发,也不是不可能。

我觉得测试驱动开发还有一个好处,就是避免事后去补测试。当代码已经完成,再根据代码逻辑去补测试,会让人感觉很无聊。因为那变成了一个完全的体力活。 如果使用测试驱动开发/设计,就完全不一样了。那相当于你给自己定了一个要现实的目标,然后再去解这个题目,就不再全有这种沮丧的感觉。同时,因为需求一直在增加或者修改,每次修改的结果,可能是一部分代码被丢弃,但因为没有写多余的代码(为了未来或者秀智商而添加的代码),甚至本来就是临时的代码,所以删删改改的,也不会有太多的心理负担。不然本来我用了很大的脑力才想出来的解决方案,巧妙又精致,结果因为需求改了,全没用了。这也是为什么程序员都不喜欢需求变更的原因之一:那些代码都是我智力的杰作,一行也不能改!而如果你的代码只是为了通过测试,就不再有那么强的神圣感了。

## AppVeyor 上的测试结果显示

本来我是想在 Travis、AppVeyor 和 DaoCloud 上都实现这个功能,结果 Travis 和 DaoCloud 并不支持这个特性。结果就只好在 AppVeyor 上使用了(这也难怪 NuGet 的 packages 都使用 AppVeyor 作为 CI 的平台了)。本来,AppVeyor 为这个功能提供了非常完备的支持:使用一些测试自动发现的机制,直接找到工程里所有的测试用例,然后执行。可这个对于 .NET Framework 很好用的功能,对 .NET Core 好像不是很友好。我在使用 AppVeyor 自带的测试发现的功能时,总是报错。后来我是直接使用 dotnet 的 test runner 来运行测试的。这样的结果就是:测试的结果不会显示在 AppVeyor 的 TESTS 页里,那样非常不爽啊!研究了一下文档,我找到了这个解决方案:

...
test_script:
- cmd: dotnet test CoreCRM.UnitTest -xml .\xunit-results.xml
- ps: $wc = New-Object "System.Net.WebClient"
- ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/xunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\xunit-results.xml))
...

首先用 dotnet 的 test runner 来执行测试,并把结果保存到一个文件里。感谢 xUnit.net 的工作,让这个事情变得这么简单。其后使用 PowerShell 脚本把这个结果 Push 到 AppVeyor 的服务器。这样就实现了结合 dotnet 的 test runner 和 AppVeyor 的测试结果页。效果非常不错。不过这里也发现,PowerShell 是个很强大的工具啊!

## 实践才是真理

理论说多少都是理论,实践才是真理。目前,针对 Profile 的 Repository 已经完成了单元测试。测试代码被放在单独和一个 project 里。一开始我的设计是:ProfileRepository 是依赖于 UserManager 的,这样,在 Controller 里就只需要依赖 ProfileRepostory 就可以了。但在测试的时候发现,这样并不方便:ProfileRepository 其实和 UserManager 没有关系,确要依赖于 UserManager,结果就是大家权责不够明确。逻辑上也比较混乱。在重构了 ProfileRepository 和 ProfileController 之后,感觉清爽多了。之前对于 Controller,特别是 AccountController 的测试感觉非常的不好写。在完成了 ProfileRepository 的测试之后,这个问题也迎刃而解了。

我会在后面的开发中,努力实践测试驱动开发,用实际的开发过程来证明这种方式的好与不好。如果你也有兴趣参与,可以 fork 我的 repo:https://github.com/holmescn/CoreCRM,一起来学习、讨论。提 issue 的朋友请 feel free to use Chinese。

CoreCRM 开发实录 —— 单元测试、测试驱动开发和在线服务的更多相关文章

  1. 敏捷开发 —— TDD(测试驱动开发)

    测试驱动开发 TDD(Test-Driven Development)是敏捷开发的一项核心实践,同时也是一种设计技术和方法. 既然是测试驱动,便是测试,测试用例先行: 首先编写好测试用例,期待值,实际 ...

  2. CoreCRM 开发实录——开始之新项目的技术选择

    2016年11月,接受了一个工作,是对"悟空CRM"进行一些修补.这是一个不错的 CRM,开源,并提供一个 SaaS 的服务.正好微软的 .NET Core 和 ASP.NET C ...

  3. CoreCRM 开发实录——Travis-CI 实现 .NET Core 程度在 macOS 上的构建和测试 [无水干货]

    上一篇文章我提到:为了使用"国货",我把 Linux 上的构建和测试委托给了 DaoCloud,而 Travis-CI 不能放着不用啊.还好,这货支持 macOS 系统.所以就把 ...

  4. CoreCRM 开发实录——想用国货不容易

    昨天(2016年12月29日)发了开始开发的文章.本来晚上准备在 Coding.NET 上添加几个任务开始搞起了.可是真的开始用的时候才发现:Coding.NET 的任务功能只针对私有的任务开放.我想 ...

  5. CoreCRM 开发实录 —— 单元测试之 Mock UserManager 和 SignInManager

    单元测试的核心就是:只测试眼前的逻辑.这就要求所有的依赖项都要使用仿类来代替,也就是所谓的 Mock Object.在测试 ProfileRepository 和 AccountController ...

  6. CoreCRM 开发实录 —— 前后端分离的重构

    虽然2月初就回来了,可 CoreCRM 一直到5月才开始恢复开发,期间是各种生活中的意外和不方便. 1. 为什么要重构 首先是一件很值得高兴的事情:CoreCRM 有了第一位 contributor! ...

  7. CoreCRM 开发实录 —— Profile

    再简单的功能,也需要一坨代码的支持.Profile 的编辑功能主要就是修改个人的信息.比如用户名.头像.性别.电话--虽然只是一个编辑界面,但添加下来,涉及了6个文件的修改和7个新创建的文件.各种生成 ...

  8. CoreCRM 开发实录 —— 基于 AntDesign 的新 UI

    上一篇说到,因为有新朋友加入,对前端开发有了新的要求.原来基于 Bootstrap 的 UI 就不要了.在网上(其实是 GitHub 上)逛了几圈,最后使用了 antd-admin 这个框架做为基础模 ...

  9. 浅谈测试驱动开发(TDD)

    测试驱动开发(TDD)是极限编程的重要特点,它以不断的测试推动代码的开发,既简化了代码,又保证了软件质量.本文从开发人员使用的角度,介绍了 TDD 优势.原理.过程.原则.测试技术.Tips 等方面. ...

随机推荐

  1. 第4章1节《MonkeyRunner源码剖析》ADB协议及服务: ADB协议概览OVERVIEW.TXT翻译参考(原创)

    天地会珠海分舵注:本来这一系列是准备出一本书的,详情请见早前博文“寻求合作伙伴编写<深入理解 MonkeyRunner>书籍“.但因为诸多原因,没有如愿.所以这里把草稿分享出来,所以错误在 ...

  2. Zend server最大化应用程序的性能、扩展性和可用性

    如果我有8个小时去砍到一棵树,我会花6个小时磨斧子”——林肯(美国总统) 你可以知道? 世界页面访问量的峰值超过7000万每分钟. CloudFare公司服务器问题,导致785000站点崩溃一小时. ...

  3. 使用Clean() 去掉由函数自动生成的字符串中的双引号

    有时候由Excel单元格函数軿凑出来的字符串会自带双引号 效果如下: 想这种这个情况,刚好我们軿凑出来的是SQL语句, 执行的时候是去掉双引号, 这时候可以使用Excel自带的函数来去掉双引号 Cle ...

  4. javascript 10进制和64进制的转换

    原文:javascript 10进制和64进制的转换 function string10to64(number) { var chars = '0123456789abcdefghigklmnopqr ...

  5. linux 启动oracle报cannot restore segment prot after reloc: Permission denied

    error while loading shared libraries: $ORACLE_HOME/lib/libnnz10.so: cannot restore segment prot afte ...

  6. gof设计模式回顾

    gof23根据讲师学习笔记回顾: 1.gof:Gang of Four;叫grasp更具有针对性,解决具体的问题; ---------------------总共分为三大类: ---------创建型 ...

  7. 日期,为下拉列表添加日期,优化,目前本人博客上最优的解决方案,之前学习的通过判断得到平年闰年,而这个是让系统自动去判断,无须if判断,代码示例

    <%@ page language="java" import="java.util.*" pageEncoding="utf-8"% ...

  8. 利用servlet产生随机数,原理是获取Graphics对象进行绘图

    public class ResonpeRandomImgDemo extends HttpServlet { int width=100; int height=30; public void do ...

  9. 自动生成api文档

    vs2010代码注释自动生成api文档 最近做了一些接口,提供其他人调用,要写个api文档,可是我想代码注释已经写了说明,能不能直接把代码注释生成api?于是找到以下方法 环境:vs2010 先下载安 ...

  10. 最简单的修改HashMap value值的方法

    说到遍历,首先应该想到for循环,然而map集合的遍历通常情况下是要这样在的,先要获得一个迭代器. Map<Integer,String> map = new HashMap<> ...