驾驭单元测试漫谈

“单元测试”,不知道是谁提出来的概念,这么多年来一直困扰着我。我一方面相信它能够确实改善代码的质量而在项目中不断的尝试;另一方面,因为尝试的过程中很少的得到正面反馈又在怀疑它的实用性。

我感觉自己是那只故事中结网的蜘蛛,网还没有结好就被风吹坏了,坏了又开始重新结,年复一年。虽然有过一些思考,但是无从下笔来记录这些年走过的路。

单元测试,说起来简单,“单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。”;做起来难,代码到处都是,要测试的那“一小段”代码在哪里?

这样的定义,可以让任何不懂单元测试的人来做一个单元测试的demo,但最终没有任何作用。为什么,因为我们的项目里有太多的代码,而代码与代码之间的关系复杂,就像让一个人去面对大海,试问每一滴水都需要进行单元测试吗?

我第一次听到单元测试这个概念是在15年前,我加入徐总的团队(现在的诺怀软件)后,这个团队对代码的质量要求很高。

当时参与的是一个胖客户端的WinForm项目,这种项目的特点是直接从数据库中读取数据,然后将其展现到WinForm窗体上,用户在窗体上保存修改的信息后,再将数据写回到数据库中。业务代码几乎都耦合在窗体的事件处理函数中。要加入单元测试,首先要对现有代码进行重构。

重构意味着修改现有的代码,意味着更多的测试工作,考虑到项目的进度,我们把重构放在项目压力小的时候,因此,最后单元测试并没有在项目中真正开展起来。

虽然没有真正展开,但是在经过几次尝试后,我感受到要并不是所有的代码都适合单元测试,随便对一个项目说“我们要通过单元测试来改善项目质量”不现实的。

问题到底在哪里呢?

假设有这样一段需求:“用户将客人订单标签拖到餐桌列表时,首先要检查该时间段内,餐桌是否已被预定。如果已被预定,弹出提示并取消当前操作;如果没有被预定,那么检查该桌子的容量与客人的数量是否匹配。如果不匹配,弹出提示并取消当前操作。最后更新订单数据,并将数据保存到数据库。”

这段C#代码大概会是这样。

private function tableChart_beforeChange (TableChartEvent event) {
  if(this.isTimeRange(event.timeRange))
  {
    MsgBox.show("改时间段已被其它客户实用");
    event.cancel = true;
    return;
  }

  if(this.getTable(event.id).capacity < this.order.capacity)
  {
    MsgBox.show("该桌太小,不能满足订单需求");
    event.cancel = true;
    return;
  }

  this.updateOrder(this.order);
}

上面的代码虽然已经很简化了,但是还是没有逃离与界面和数据库的耦合,面对这样的代码,该怎样进行单元测试呢?

如果把时间向前拨回到15年前,我可能是这样思考的

这个事件包含了一个完整的业务逻辑,因此要用一个测试来保证其完整性。要调用tableChart_beforeChange函数,我得首先创建这个函数所在的类的实例;然后这个类是窗体类,要初始化这个窗体类,得先初始化包括数据库在内的配置;然后,这个数据是业务数据,我得保证数据库中有符合测试要求的业务数据。当我想到这一点时,我崩溃了 。对了,不是有mock框架吗,可以mock数据库的访问啊。那我先看看这个窗体类初始化的时候访问了哪些数据,天呐,是直接在代码中通过SqlConnection,通过拼接sql的方式来操作的数据,无法mock,我又崩溃了。

由于碰到了这样或那样的问题,我第一次单元测试应用是失败的,我得出结论是要使用单元测试,就得改变代码的写法。

单元测试要测什么样的代码呢?

  1. 看得见的代码
  2. 包含了if, switch等条件判断的代码
  3. 包含了计算的代码

上吗的2和3是我从前辈那里借鉴来的,第1点是我自己总结的。

回到最初我遇到的那个难题,其实就可以用第1点来解决。这段代码的逻辑中包含了3个函数调用,这3个函数后面的代码是什么,我们看不见,因此将其mock掉,MsgBox.show,这是界面框架提供的函数,后面的代码看不见,也将其mock掉。这样让我最初崩溃的两个问题就轻松解决了。

写到这里,新的问题又来了,上面的代码逻辑这么简单,简单就不容易出错,有必要进行单元测试吗?

的确,我最初也有这方面的困惑,一方面觉得代码简单,出错的概率很低,没有必要进行单元测试;另一方面,遇见很长的一段代码,又会觉得逻辑太复杂,对其写单元测试代码需要花费很大的代价,可能会得不偿失,还是不写吧。

这里似乎是一个关于工作态度的问题,即“简单的不屑做,复杂的不会做”,我也曾用这句话来责备自己。但后来在学习结构化设计时,了解到了“扇出”和“扇入”的概念,发现这个概念很适合来解决这个问题。

模块的“扇出”大,说明模块复杂;模块的“扇入”小,说明模块的复用程度小。

对于“扇出”大的模块,应该适当增加中间模块来降低其复杂性。这种“古老而朴素”的思路不是很适合来解决单元测试中遇到的被测试代码太长太复杂的问题么?

我上面说“思路”是因为它只是指出了方向,但不是方法,它没有具体说如何增加中间模块。的确,“增加中间模块”的方法应该到“重构”和“设计模式”中去找。

写到这里,我发现自己又回到了“写”代码的原点上了。

设计得好的代码,自然就容易进行单元测试;但反过来说好像也行,即单元测试做得好,代码也不会查到哪里去。

如果将写代码看作“知”,将单元测试比作“行”;知行合一才是“妙”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涵树_fx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值