kotlin+mockk:真正的玄铁重剑

⚠️
最近仔细研究了一个Github上的某开源库,尤其仔细研究了其单元测试,收获很多。具体哪个库就不说了,请勿对号入座。

kotlin+mockk是真正的苦口良药,是真正的玄铁重剑。
相比某些落后的语言和落后的框架来说,kotlin+mockk不仅能实现业务功能和编写测试,而且能全面的锻炼开发人员和开发团队的各方面能力。

甚至懒得过chatGPT的错别字检查……毕竟这个问题的必然性太显然了,问题对项目质量的危害也太显然了……


Repo的经历

投入更多资源

也没什么,之前穷,Repo的CI的Runner只有2核CPU跑build。并发量很低,构建量大,每次都要排很久的队。
我也也没什么能力,怎么优化测试我也不太懂,有没有多余的无用的测试我也不知道。但菜有菜的做法,我直接提供一台更加强大的机器,直接上了一台12核的机器,允许4线构建同时运行。

所以,这里引入了一个变更:运行测试的机器变大了

初期的顺利

这个Repo毕竟是明星Repo,也不敢直接就把这个Repo放到新的机器上跑。
出于对这个Repo的膜拜,我首先将这个Repo相关的其他Repo放到了新机器上运行。整体效果非常好,毕竟多了6倍的计算资源。而且这6倍的资源是在同一台机器,那么如果只有一个构建,那么这个构建就能独占所有资源。美滋滋。

所以,这里有一个事实:所有的小Repo都能稳定运行

鹤立鸡群

最终,我怀着极大的信心,以及极大的荣耀感,终于将这个Repo切换到大机器上运行。
结果这个Repo的构建开始变得不再稳定。10次构建中有8次里单元测试都会失败,而且每次失败的单元测试都不一样。

所以,koltin+mockk的厚重体现出来了:稍有放松就开始崩溃


不稳定的原因

即使现在,我仍然没有从根本上解决不稳定的问题。所以下面说的原因,不一定是全部问题,甚至不一定是问题。
因为,失败是随机失败,我找到了一个原因,提交了代码之后,构建虽然通过了,但是我不能保证到底是我运气很好,还是确实解决了问题。(3000组测试,总不能全部通读一遍吧?)

unmockkAll

Don’t forget to unmockkAll
In a project I’m collaborating we are using Mockk and many other frameworks to do unit tests. This is a very nice framework that help us…
参考文章

简而言之,有些测试其实是需要在跑完之后调用unmockkAll的。如果漏掉了,运气好,新加的这个测试跑在最后或者本身mock的东西比较独立,那不会失败。但运气差一点,或者稍微讲究一点,就很可能会失败。

所以,不稳定的原因之一:大机器、多构建并发的环境,导致gradle运行测试的顺序开始变得趋于随机化。然后忘记unmockkAll的问题就暴露了出来

参数化测试的参数是mock

这个问题我不是100%的确定是个问题,反正改了以后,好像通过率提高了(也可能是改了之后运气变好了)。

有些测试是参数化的,这个其实没什么特别。结合前面的unmock,我发现写这个参数化的人还是有点水平的,至少它没有忘记写@Aftereach。
但是呢,有意思的是,它的参数生成函数是在一个静态函数里,而且生成的参数是在静态函数里直接mock的。

显然,这个静态函数是返回给junit一个list说我有那些参数。测了一下发现,这个静态函数确实只会在初始的时候调用一次。
然而Aftereach的unmockkAll可是每一个测试跑完就会调用一次的,也就是有10个参数,就会调用十次unmockkAll。但是生成mock只有最开始的时候调一次……

我个人不太确定unmockkAll和clearAllMocks的区别,毕竟mockk博大精深,我也不懂。反正我给他参数从对象改成lambda了……

所以,不稳定的原因之二:参数逻辑写的太骚了

FeatureToggle没有清理

简而言之,和前面unmock一样,跑完之后其实是需要清理临时设置的toggle(是的,允许在runtime改toggle,然后改了之后还会有持久的副作用)。

所以,不稳定的原因之三:大机器、多构建并发的环境,导致gradle运行测试的顺序开始变得趋于随机化。然后忘记清理toggle的问题就暴露了出来

总结

说到底,就是写测试产生了太多副作用,然后忘记了清理。


短期解决

两种方式,都不是很好,但大体来说,一清一浊。

清:

每次发现失败的测试,就添加防御式的@beforeAll和@AfterEach(当然要把4个都用全了也不是不行)。在里面清理mock和toggle。

这样一来,一方面当前失败的这一组测试一定会安全。另一方面,这个测试清理之后,运行在它后面的测试会有一个更干净的环境。
所以,这样可以很快提高测试的通过概率。

浊:

直接gradle里面指定,让测试按字典序跑。只要字典序能跑过,就不管了。

这个当然是不好的。但如果因为测试一直不太稳定,导致CI的信心受损,某些人就开始不修测试,甚至直接不写测试的话。这个浊流就可以变成一个很好的武器。
至少,可以最快速度回复稳定,使对方没有合法理由忽略CI报错。

另一方面。既然能字典序,改一下就能变成完全随机,不依赖机器的随机。

至清:

找。3000组测试通读一遍,或者以失败的测试为线索顺着找。找出具体是哪一组测试没有清理。

好处当然是从根本上修复问题。坏处就是代价太大了。

至浊:

改回小机器。

“之前测试一直稳定运行,现在换了机器就挂了。你凭什么就能说是测试的问题?怎么就不能是你机器安装的不对?
什么你加了清理就通过了?那既然修好了,就不要再说了。”

所谓“官场无朋友,朝事无是非。”如果身处一个全无是非,看似一团和气,实际上不知道什么情况的环境。走至浊至暗的路子也是个办法。


长期解决

Junit、mockk的流程周期的把握

人笨不能怪刀钝。mockk提供static mock自然有其道理,说到底是人用的不够好。

所以作为开发要加强对mockk的学习,作为项目管理者要注意考核项目成员对mockk的了解状况,作为企业在市场对mockk还不甚普及的时候要做好新员工mockk的专项培训
尤其要切记避免片面的、单一的的注重人员的开发能力、架构能力和解决问题的能力。

当每个人都对mockk非常了解,融会贯通之后,自然就会懂得测试之后清理副作用的重要性。
自然也就能100%避免忘记清理的问题,自然也不会出现3000组测试里面藏着1个有问题的测试。

Code Review的重点

同理,在平时code review过程中,也应该相应的重视测试清理相关的review。

不能片面的、单一的只看重代码的结构、可读性、扩展性和正确性。
不能因为只是在300行的测试文件里添加一个新的测试就只看新的测试而不看整个测试文件之前是不是没有unmock。毕竟测试和代码一样,是一个整体。你这里添加一个测试,用了mock。但前面的测试人家没有mock,自然也就不会有unmock。
所以,Code review一定要全面,要细致,要做到不怕麻烦,把整个前后相关的代码都一并review

毕竟3000个里藏1个定时炸弹的拆除难度太高了。自然我们就要通过review,争取100%的把问题扼杀在摇篮里

贵在坚持

既然选择了kotlin+mockk。而这样一个组合难得的给我们团队的实践、个人的能力都提出了更高的要求。
那我们当然不能因为要求更高更难就选择退缩
事情就怕一个坚持,只要坚持努力,不断学习mockk,不断细化code review的内容。
就一定能彻底避免忘记清理副作用的问题。

而像传统的java和mockito,就缺乏拼搏精神。为了安全,几乎就不怎么鼓励搞静态,什么东西都非要放到对象里。
长期下来,一点挑战没有,人就废了。


所以kotlin+mockk必将成为行业标配。
因为它不仅着眼于实现,而且竟然还反过来促进开发人员和团队不断自我鞭策。高,实在是高。

蜀ICP备19018968号