前言
在Visual Studio 2012 中,针对Unit Test 的部分,有一个重要的变动:
原本针对「测试对象非public 的部分」,开发人员可通过Visual Studio 2010 自动产生的accessor ??来进行测试。但在Visual Studio 2012 中,将此功能移除了。
Accessor ??其背后的原理,是将对象通过很「脏」的反射方式,把对象内所有的东西public 出来。并且Visual Studio 在更新对象后,进行与设计测试时,会帮你做同步产生accessor ??的动作。(实际的原理我没有深入研究,也不太确定。但基本上的概念就是如此)
这个原本被认为很方便、实用的功能(包括我很久之前写测试时,也是这么认为),很抱歉,在Visual Studio 2012 后已经被移除了。
接下来本篇文章将会说明,单元测试是否应该对测试对象非public 的部份,进行单元测试。
单元测试的意义
一言以蔽之:「单元测试就是用来模拟外部如何使用测试目标对象,验证其行为是否符合预期」。
因此,有个重点是:外部如何使用测试目标对象。
让我们回到Object-Oriented 的封装原则,封装的用意在于:
- 隔离出对象的内部与外部。也就是定义「对象的边界」,以及定义「外部可视部分」。
- 将外部使用端,不需要了解对象的内部信息,封装起来。也就是「封装细节」。
- 将对象内部的变化,封装起来。也就是「封装变化」。
有了对单元测试与封装的认知后,接下来说明,为什么单元测试只需要针对测试目标对象public 的行为,进行测试即可。
只测试Public 行为?
根据单元测试的意义,以及封装的用意,代表着「外部使用者原本就不需要了解,也根本不了解,测试目标对象非public的行为」。单元测试既然是模拟外部使用端的动作,那当然只针对测试目标对象public的行为进行模拟与验证。
但一些朋友肯定有些疑惑,那非public 的method 该怎么办?不测吗?那code coverage 怎么提升?要怎么知道这些非public 的行为有没如同预期般运作呢?
有这些疑问是正常的,因为我一开始也是有一模一样的疑问,但开始接触TDD 之后,反而更加了解了Unit Test 的本质。
所谓的非public 的行为,其存在的原因,一定是因为某一些public 的行为会用到这些private 或protected 的method,如果对象中存在着跟public method 无关的private 或protected method,那在设计上就是个问题,这些非public 的method 根本就没有存在的意义。因为外部使用测试目标对象时,完全不会用到这些method,就像声明了变量却不去使用它一样,没有意义。
而当私有或受保护的方法与public 方法有关时,那针对公有方法的单元测试便会涵盖到这些私有或受保护的方法,它们就是公有方法的一部分,对外部使用者来说,根本分辨不出来什么是私或受保护的,因为只关注在对象外部可视行为上。
所以,在实作单元测试上,倘若测试对象一个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 所提倡的精神,如果所有使用行为都符合预期,就代表功能完成了。而且依据测试来撰写的生产代码,几乎不会出现测试涵盖不到的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 就一直困扰我很久,虽说脑袋中有点轮廓,却一直无法明确厘清。
补充
这边有一篇写的很不错的文章,讲的相当全面,包括概念、现实上的考量、过程中的考量,都写得很清楚。请参考:Testing Private Methods with JUnit and SuiteRunner
2012/11/09 补充:VS2012 将accessor ??与自动产生单元测试代码的功能移除的另一个原因是:因为原本accessor ??的产生机制,与MS Test 的耦合度太高了。(在Visual Studio 2012 后,期望可以很弹性的与其他Unit Test Provider 结合。)
读者的疑问
针对读者的一些疑问,我就补充在文末,大家若还有什么想了解或发问的,欢迎留言。
Q1. 文章上只提到了 public, protected, private,那麼 internal 呢?
答: 这是一个很棒的问题,因为我文中的确没提到internal 的部份。
首先internal 的定义/用意,是指在同一组件内才能看的到,也就是我这对象希望在我这组件里是公开的,但组件外的人看不到也用不到。(这样设计可以有效控制相依范围)
而单元测试如前面所说,是针对「对象」的互动,来进行模拟使用。那声明成internal 的对象,到底要不要测试,当然要,因为的确有其他对象会使用它,我们就要思考:「怎么使用它」。
但一般测试项目的角度来看,是参考生产代码 的library,所以对测试项目的角度,是看不到生产代码 里面声明成internal 的对象的,??但我又想去测试生产代码 中internal 的对象,该怎么办?
在.NET中相当简单,只需要通过:InternalsVisibleToAttribute这个属性设定即可。将生产代码 library指定给test project可见,就可以解决这个问题。
Q2. 若没针对private测试,当发生问题时,我怎么知道是哪一段code错了?或是它没被涵盖到,就代表没有受到测试保护。
答:这个问题,就是慢慢消化这篇文章,并实际动手做之后,就会渐渐的拨开云雾见青天了。
当只用测试的思维来看,那不去「针对」private method 测试,是一件很奇怪的事,因为它活着,但没有测试可以马上知道它对不对。
这也是跨入TDD 的其中一道门槛。回过头来看前几篇的宗旨,系统的存在,到底为了什么?
为了可以正确的满足使用者的需求,外部使用的需求。既然用了对象导向来设计,既然把这些东西封装起来,外部的使用者就根本看不到、用不到,也不该看到用到。而我们封装的意义就在于封装变化。这时候用其他方式,硬干进去对象中去测试private method,也只是增加自己未来的负担,因为它肯定会一直变。
原本private 的改变,可以几乎不影响任何部分,除了对象本身内部。所以它可以放变化。
现在外面看的到这个方法,你就不能轻易改变,一旦要改,可能会影响许多测试程式,反倒是生产代码 不会有太多影响。但测试程式如果因此要维护或是要重写,这就都是根本没必要的东西。
最后,如??果你用TDD的方式开发,就根本更不会碰到这个问题。
因为,你只针对public 行为,来进行预期,永远切入点都是撰写public 的内容。大概只有重构的时候,会出现private 跟protected。而这个时候,被放到private 的方法,当然是你原本放在public 方法内的内容。
那如果原本public 方法code coverage 是100%,那也不会因为你搬到private,code coverage 就变成50%。如果出现了因为重构,就没有涵盖到的范围,那就是over design 的bad smell,是个征兆。
这边就是需要搭配TDD 与Refactoring 的手法,才能一体成型,享受其美妙之处而无后顾之忧。再强调一次,private / protected的方法内容,在TDD里面,基本上都是因为refactoring的extract method所产生的,都是一些原本放在public / internal的function内容。而不会是直接动手去写private function,除非你是top-down的先订出程序的框架。但最终,private function仍属于public function内容的一部分。
所以要特别满足的应该是:您是否有针对外部可见的行为,进行了所有具代表性的情境来做测试。如果真的涵盖了所有,包括exception handling,那么这个对象内,没被涵盖到的部份,基本上都可以删除了。绝不会对外部使用造成任何影响。
备注:这个系列是我毕业后时隔一年重新开始进入开发行业后对大拿们的博文摘要整理进行学习对自我的各个欠缺的方面进行充电记录博客的过程,非原创,特此感谢91 等前辈