8.9 单元测试
“春色满园关不住,一枝红杏出墙来。”之所以想起这两句,是因为虽然最近这两周项目紧张,我们一直在赶进度,但是忙里偷闲,我们还是做了一件对Android项目而言很有意义的事情,那就是单元测试。
开始讲述我的故事之前,先来扫扫盲:
·什么是单元测试?请参见文章:http://baike.baidu.com/link?url=DtllYiDKetRaM2z1uKgLG_BDGYDYU3gNFzOQnd13i9k7lqnLHEelYuoAVd0WwYMy 。
·TDD(在微软时,我们戏称为“踢弟弟”)请参见文章:http://www.ibm.com/developerworks/cn/linux/l-tdd/index.html 。
·Android单元测试的相关文章。请参见文章:http://www.oschina.net/question/54100_27061 。
·iOS单元测试的相关文章。请参见文章:http://blog.csdn.net/fengsh998/article/details/8109293 。
有人认为单元测试(UT)是测试人员写的,错!大错特错!!测试人员可以写TestCase,写UAT case,但就是写不来UT。UT是开发人员写的。
面试时发现,绝大多数的App开发人员,没有写过单元测试,原因有三:
·在客户端这个领域,业界没有写UT的风气。
·基于UI的单元测试,不知道怎么写。
·高强度开发,没有时间写UT。
其实,在客户端写单元测试的好处有很多:
·对蕴藏在客户端中的复杂逻辑或者算法,如果有相应的单元测试,可以确保每次小的逻辑变动,而不用再手动测试其他情况,只需要跑一遍所有的UT即可。
·单元测试要求编码时将UI与业务逻辑相剥离。但凡做不到的,都是代码写的有问题,耦合性太高。
但是,绝对不能以偏概全,为客户端的所有代码都加上单元测试,那是不现实的。我的经验是,只为那些复杂的业务逻辑(有很多if-else分支语句)写单元测试。
下面以验证身份证号码是否有效作为例子,来介绍如何编写单元测试。项目请参见我博客上的源码 [1] 。
身份证的业务规则如下所示:
1)15位或18位长度。
2)15位,必须全数字。
3)18位,前17位必须全数字。
4)检查出生日期是否为有效的日期。注意18位和15位的取值规则是不一样的。
5)检查18位的最后一位是否有效(这个值有可能是X)。
上述业务逻辑的实现,请参见Utils类的isIdCardNumberValid方法。可以看到这个方法非常复杂,有太多的if-else逻辑判断。动一动牵发全身,导致后面的逻辑有问题。代码量很大,由于我这一节介绍的是单元测试,所以就不贴出来了,大家可以去TestCode项目下去看具体的实现。
如果我们想增加一个新的业务规则,或者发现某个bug而对上述某个规则进行了修改,那么该如何确保其他业务规则不受影响呢?
只有单元测试能解决这个棘手的问题。
于是我们为每条业务规则都准备了若干单元测试用例,每次做出修改,都把这些用例全都执行一遍,这些用例集中放在TestIdCard类的testIdCard方法中,如下所示(截取部分代码):
public void testIdCard() throws Exception { // 测试长度为 0或者输入为空的情况 Assert.assertEquals(AppConstants.IDCARD_LENGTH_SHOULD_NOT_BE_NULL, Utils.isIdCardNumberValid("").getIdCardDesc()); Assert.assertEquals(AppConstants.IDCARD_LENGTH_SHOULD_NOT_BE_NULL, Utils.isIdCardNumberValid(null).getIdCardDesc()); // 测试长度不为 15或者 18的情况 StringBuilder idCard = new StringBuilder(); for (int i = 0; i < 20; i++) { idCard.append("1"); if (idCard.length() == 15 || idCard.length() == 18) continue;
图8-8是Android单元测试用例的执行结果,标记√的表示单元测试通过,标记×表示测试不通过:
我们看到,具体错误发生在testIdCard这个方法上,双击它能定位到具体有问题的测试代码,一路跟踪到Utils类的isIsCardNumberValid方法,发现问题出在对身份证号码的最后一位校验上,代码中逻辑仅支持小写的x,对大写X并不支持。
把这个问题上升到需求层面,对于用户而言,输入身份证号码是不要去区分大小写的,所以这确实是一个bug,于是我们修改这个逻辑,比较时不区分大小写。再次运行单元测试,如图8-9所示,可以看到所有测试用例都通过了。
图8-8 单元测试的执行结果
图8-9 单元测试的执行结果
通过编写单元测试发现bug、修复bug的例子,证明了单元测试是确实有很大帮助的。
说起单元测试,往事历历在目,有甜蜜,有心酸。接下来是八卦时间,大家可以去抢沙发和板凳了。感谢读者们花钱买我写的书,接下来我给大家分享一个苦逼程序员的故事。
话说我每天加班都要到晚上10点多,终于有一次约到了女神吃饭,我还清晰地记得那是第一次下班的时候天还亮着。6点半我已经在出租车上了,车上还坐着我的一个兄弟,他要蹭我的车去地铁站。快到目的地的时候,女神微信我说已经在餐厅排队等位子了,再后来跟我说已经排到了位子就等我过去了。这时悲催的事情发生了,还在公司的兄弟打电话来说线上出事了,有个模块频繁崩溃。我当时好纠结啊,去约会还是回公司?最后还是咬咬牙,让司机调头开回公司。我还记得在出租车上和女神解释放鸽子的原因的时候,女神只回了我六个句号,然后就再也没有然后了。
当然和我同车的那个兄弟也同样悲催,因为他被我带回了公司一起查问题。多年之后,我们喝酒时说起这件事,仍然感慨万千。
我们到公司后发现,问题时有时无,并不稳定重现。那是一个用Comparator实现的排序算法,数据源来自MobileAPI,我们要把其中状态为0的数据都排到前面,状态为1的数据都排到后面。
但是Comparator排序算法写的有问题,而这个问题很隐蔽,仅在某些特定的情况下才会发生崩溃,而我们在做功能测试时,并不包括那些特殊的测试场景,所以只有等到发版后根据线上的真实数据发现问题了。
想要规避这种情况的发生,只有写单元测试。由开发人员准备各种测试数据,以证明算法的正确性。
[1] 下载地址:http://www.cnblogs.com/Jax/p/4656789.html。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论