【转载】 单元测试:是否针对非 public method 进行测试?

前言

在VisualStudio的单元测试中,对于非public的函数,可以通过VisualSTudio自动产生accessor来进行测试。但 Visual Studio 2012却把这个功能给移除了,让不少开发者感到不便。本篇文章就来说明,单元测试是否应该对测试物件非 public 的部份,进行单元测试。

 

单元测试的意义

一言以蔽之,「单元测试就是用来模拟外部如何使用测试目标物件,验证其行为是否符合预期」。

因此,有个重点是:外部如何使用测试目标物件。

让我们回到 Object-Oriented 的封装原则,封装的用意在于:

  1. 隔离出物件的内部与外部。也就是定义「物件的边界」,以及定义「外部可视部分」。
  2. 将外部使用端,不需要了解物件的内部资讯,封装起来。也就是「封装细节」。
  3. 将物件内部的变化,封装起来。也就是「封装变化」。

有了对单元测试与封装的认知后,接下来说明,为什麽单元测试只需要针对测试目标物件 public 的行为,进行测试即可。为什麽 Visual Studio 2012 要把 accessor 的功能移除。(不过这纯属我自己从单元测试意义当出发点的推论)

 

只测试 Public 行为?

根据单元测试的意义,以及封装的用意,代表着「外部使用者原本就不需要了解,也根本不了解,测试目标物件非public的行为」。单元测试既然是模拟外部使用端的动作,那当然只针对测试目标物件 public 的行为进行模拟与验证。

但一些朋友肯定有些疑惑,那非 public 的 method 该怎麽办?不测吗?那 code coverage 怎麽提升?要怎麽知道这些非 public 的行为有没如同预期般运作呢?

有这些疑问是正常的,因为我一开始也是有一模一样的疑问,但开始接触 TDD 之后,反而更加了解了 Unit Test 的本质。

所谓的非 public 的行为,其存在的原因,一定是因为某一些 public 的行为会用到这些 private 或 protected 的 method,如果物件中存在着跟 public method 无关的 private 或 protected method,那在设计上就是个问题,这些非 public 的 method 根本就没有存在的意义。因为外部使用测试目标物件时,完全不会用到这些 method,就像宣告了变数却不去使用它一样,没有意义

而当 private 或 protected method 与 public method有关时,那针对 public method 的 Unit Test 便会涵盖到这些 private 或 protected method,它们就是 public method 的一部分,对外部使用者来说,根本分辨不出来什麽是 private 或 protected,因为只关注在物件外部可视行为上。

所以,在实作单元测试上,倘若测试物件一个 public method 中,涵盖了一个 private method,而 private method 中与外部物件或服务相依,那麽在测这个 public method 时,要连 private method 中相依的 interface ,都要撰写 stub object 来模拟才行,这也是为什麽单元测试被称为白箱测试的原因。但还是得强调一次,外部使用者是无法分清楚哪一部分是 public method 内容,哪一部分是非 public method。

总结上面的说法,非 public method 的测试涵盖率,是依据 public method 呼叫时的 input 来决定。

有没有可能,当 public method 该测的都测了,甚至 public method 主体内容涵盖率都 100% 了,非 public 的部分涵盖率却很低?当然有可能,但这要釐清一下,没有被涵盖到的部份,是属于什麽样的程式码。

如果在非 public method 中,没被测试覆盖的部份,是防呆、断言之类的程式码,那麽是属于正常的情况。因为可能在呼叫非 public method 之前,就已经先防呆了,导致非 public method 中的防呆永远不会发生。但,因为系统的健壮性考量,该断言、防呆、验证的部份,还是不能少。因为不会知道未来其他方法呼叫前,有没做好防呆的部份。

那麽,在 private 或 protected method 中,非防呆、断言的程式码,却又没被涵盖到部分呢?这是个警讯,代表着这些程式码可能是 over design,或是根本没有用处。因为这个物件所有对外的行为,所有的可能性,都模拟过一次了,却都不会用到这些没被涵盖到的程式码,这不就代表「这些程式码目前用不到」吗?YAGNI 原则就是在说这件事:「You ain't gonna need it !」

只要 public 的行为如同预期,即使 private 或 protected 的 method 是 hard-code,是很没弹性,是很愚蠢的写法,对外部使用来说,根本就不在乎,因为无感。

这也是 TDD 所提倡的精神,如果所有使用行为都符合预期,就代表功能完成了。而且依据测试来撰写的 production code,几乎不会出现测试涵盖不到的 code,因为 production code 是为了满足测试而撰写的。不需要存在用不到的 production code,因此,也可以避免 over design 的情况。

 

针对非 public 行为测试又如何?

上面那一段的说明,肯定还是无法说服所有人,「为什麽要把已经存在的功能移除?」

不用 accessor 的人大可不用,但已经在用,或真的得用的人,还是希望可以在 VS2012 中继续使用。

回到封装的用意上,「封装变化」一直是面向对象设计中很重要的设计原则。那些针对 private 与 protected 进行单元测试的朋友,有没有过「因为一些需求异动,导致单元测试程式就需要跟着重新调整、设计或修改,而且频率与范围导致测试的维护成本增加不少」的经验。如果有,这就是为什麽不希望 developer 去针对非 public method 写单元测试的原因。

着重在非 public method 的单元测试,说穿了只是写给 developer 爽而已。因为要封装变化,才会把这些内容变成 private 或 protected,以期望变化时对外部使用者来说,呈现无感,也就是降低耦合,也就是最小知识原则。

现在单元测试却透过某些机制,来存取这些封装起来的行为,不是自讨苦吃吗?原本就知道,这些东西很可能会一直变化,却又去存取它,测试它,导致单元测试因此维护与异动频率增加,这不就违背了封装的用意?对使用来说,根本不关心这些变化,却因为单元测试用髒方法硬干到这些不公开的行为,导致测试成本增加,进而导致一些不明就裡的 developer 喊出「测试很花成本,时间增加很多,很难维护」。我只想说:「这不是南北拳的问题,是你的问题。」

 

结论

说真的,刚知道 Visual Studio 2012 把 accessor 功能拿掉,我也一整个相当吃惊,觉得要强迫 developer 用 TDD 方式开发,也不用做到这麽绝吧。

但将面向对象的原则、TDD 的精神、单元测试的基本意义结合起来后,有了上述的思考历程,就觉得只测试 public method,不建议测试 private 与 protected method,是一件正确且重要的事。

所以将这样的思考与推论过程,分享给各位朋友参考,不一定完全符合 Visual Studio 2012 移除 accessor 的原因,这只是我自己的理解与想法而已,但从我一开始接触单元测试,怎麽测 private method 就一直困扰我很久,虽说脑袋中有点轮廓,却一直无法明确釐清。

可以的话,后面几篇文章,会再针对 production code 的可测试性,来说明如何透过单元测试以及程式码的可测试性,来检验与提昇程式码的品质。

 

原文请访问:http://www.dotblogs.com.tw/hatelove/archive/2012/07/19/why-you-should-not-write-unit-test-with-private-and-protected-method.aspx


原文地址:https://www.cnblogs.com/TianFang/p/2602121.html