最近一直在思考单元测试,也研究了一些测试代码中出现的怪现象,当然测试代码中的怪现象和生产代码中的怪现象,和生产代码的design,也是息息相关的。经过深入思考之后,现总结如下。
1、测试代码和生产代码究竟哪个对Quality要求更高。首先,很多人说测试代码要和production代码有一样的quality,但测试代码并没有真正运行在prodution环境上,而只是在CI环境中运行,一个从来没有机会运行在production环境中的代码,你怎么知道它是不是production quality呢?这是概念上的悖论。第二,当你在production上发现了一个bug以后,你为了debug这个问题,你是去看production代码呢还是去看测试代码呢?肯定是去看production代码。由此可见,测试代码此时并没有production代码那么有用。
2. 我们写任何code,做任何design,都是为了使得quality和productivity,在这个特定的要解决的问题领域的条件下求得平衡,从而产生最大的效益。我认为测试代码,它毕竟不是production代码,所以在测试代码上可以适度更偏向productivity。
3. 理论上,当我要去单元测试一个系统的时候,而这个系统有无数的class,此时如果追求最大的quality,或者test effectiveness,那么最为纯粹的做法是我test每一个class,同时我的test只测这个class的内部逻辑,对于这个class用到的其他class,统统mock掉,并且往往为了解构我还需要为mock创建interface。参见这个视频: Integrated tests are a scam (https://vimeo.com/80533536 )。 我承认这样在理论上的确可以得到最大的test effectiveness,但这只是理论上的,因为这样的做法带来了巨大的productivity开销,在现实中很难使用。这么写的话,每一样东西都要mock,而且mock还需要精心设计,这对开发效率的影响实在是太大了。
4. 我也可以把trade off的天平导向另一头,追求productivity。我可以让一个test就涵盖了好几个class,这样我就可以少写很多的test。而且我的一个test覆盖多少个class完全由写test的人来控制,可多可少。我可以把这些被test的class合在一起叫做一个component。然而这也带来了test effectiveness的问题。因为component在UT这个层面上根本无法界定,3个class可以是一个component,100个class也可以是一个component。你又如何保证你的test就一定覆盖了component里面的所有的代码执行路径呢?对于review你代码又对你代码不熟的人,你又怎么让他们相信你这个component里面的class也一并被你的test都测到了呢,毕竟你并没有对你component测试路径内部的相关class写任何assert。你说我测试路径上已经经过了这些class所以不再需要assert了,但是review你code的其他人不是这么想的,他们对你的code不熟,不能很快看清,那么如果一个code没有其他人来能一起判断验证的话,只凭写code人自身的保证,这个code我觉得也是有潜在风险的。如果这个code不能被其他人很快的看清,那么反过来说明了这个测试方法的readability是有问题的。此外,在component的边界上也需要mock,但当component比较大的时候,判断component的边界就成了另一个降低productivity的问题了。
5. 我觉得我们要站在天平的中间看问题,既要保证productivity,又要make sure test effectiveness 仍然在可控范围之内。那么,首先我仍然坚持为每一个class写test的做法,因为只有这样我写的test才是在readability和quality上可控的,因为这样的test目标比较小,dependency比较少,写test的人用不着考虑太多一个class的逻辑以外的问题。每个test都相当的程度的standalone,这样才能减少dev的context switch,focus在当下的那个小目标上,达到flow的精神状态,提高productivity。然后,我不想把要测的class的所有dependency都mock掉,我希望所有的东西都是真实的,我on demand mock我要mock的东西就行了。这样我就可以最大程度的减少写mock设计mock的时间。请注意,这里的概念是我虽然全部用的真实的,但我的focus的点始终是我要test的那个class,我所有的assert都是为我要test的那个class的逻辑写的。同时对于那些dependency的class,虽然我用的都是真实的,但是我会对那些class的每一个也都写test,不是说那些class就不测了。我认为这是一个在2者间的trade off。当然这个trade off也会带来问题,比如一个class A改了code,那么所有用到class A的那些class的test可能都会挂掉。但是这个问题,首先取决于在你的系统里是不是需求经常变化,如果需求不是经常变化,那么我认为这个问题发生的次数比较低,而且重要的是在class A被改之前所有的test都是能work的因为毕竟是你sync了code,而sync下来的code所有的test肯定都是通过的,所以只可能是你自己改了class A。我们一直倡导一个pull request尽可能不要包含太多代码改动,尽可能多次check in,如果是这样做的话,那么对于这次的代码改动,你应该不会费太多时间去fix这个问题。甚至我们也可以用TDD来缓解这个问题的发生。总之,有些问题是可以用适当的design来化解的,但是如果系统不灵活那么连这种design的机会也都没有了。
6. 上面几点讲了宏观,这一点讲讲微观。在写具体test的时候,effectiveness最高的是把你要test的method看作一个黑盒,设计不同的输入来assert它的输出。你不能假设你了解它的内部实现(虽然你了解),因为一旦你把它看成了白盒,你的test的关注点很可能就不知不觉的从要test出production code里的问题转移到了如何让你的test code去迎合你的production code,这样你的test就没有effectiveness了。其实上面第3点的一个问题就是写了很多mock以后,dev为了把test快点写完就随便写了这个mock的实现来迎合prod code的实现,这反而降低了test effectiveness。唉,你看,所以事情往往都是很矛盾的。还有一点是,尽量从public方法入手构造input数据开始test,只有对public进去很难覆盖到的,再对这些很难覆盖到的private进行test,因为在一个class的level进行test,所以不必对每个method都单独写test(注意这里和上面每个class都要test的不同),这样提高productivity。
7. test effectiveness和coverage没有半毛钱关系。我可以coverage 100%但是一个assert没写,这样我就没有test effectiveness。所以test effectiveness只和assert的多少有关系,assert的越多,我对这个test的信心显然就越足。然而assert的越多,必然coverage越大。coverage越大,不一定assert的越多,也不一定这些assert就不是用来糊弄人的。
希望我把关于这个问题的trade off的点在哪里的我的思考讲清楚了。
网友评论