前端测试的反模式

过于关注实现细节的测试

在为前端项目编写测试用例的时候,你也许和我一样,曾遇到过以下困扰:

  1. 明明进行了功能正确的改动,测试却挂了。修复测试有时候得认真阅读各种mock的细节,或者去了解很多本没有必要知道的代码逻辑。最后修测试花的时间比进行业务改动花的时间还要长(甚至长很多)。

  2. 对代码进行提取抽象之后,为各个组件或函数添加测试,实际上是用测试工具的API去重复 业务代码的内部实现逻辑(有时候还很麻烦!)。任何正常的重构都会导致测试失败,你本来希望测试能告诉你什么样的修改是对的,结果现在测试只能告诉你代码确实有被修改。

  3. 测试写好,覆盖率提高,本应信心十足地认为代码变得健壮了,可是扪心自问,你知道自己写的这个测试弱点在什么地方,或者说还有多少细节没有涵盖。你精心模拟了一个条件,去触发逻辑流程,并且测试通过,可是在真实的浏览器交互中用户也许并不能触发这个条件。因此,同样的道理,你在自己的代码通过了他人写的测试之后,也不能确定真实场景下没有问题,只好把后续的重任交给QA。

造成上面三个问题的原因不止一个,但测试过于关注实现细节在我看来是最主要的。

第一个问题,明明是正确的改动,可是测试不止是验证业务功能,还对实现细节提出了不该提出的要求,比如要求你的函数接受跟以前一样的参数,返回值必须是字符串而不能是数组等等。可是这个函数只是实现流程中一个小小的环节,也许在下次重构时就会不复存在。

第二个问题很类似,如果测试代码去重复实现细节,不管进行正确还是错误的重构,你都得把测试改一遍,那原先的测试又能提供什么价值呢?

第三个问题有时发生在,测试的实现细节,不能覆盖整个真实交互流程的时候。用户点击的是屏幕上的button按钮,而测试的起点是onClick事件被触发。后面的逻辑被验证成功,可问题偏偏发生在点击环节,真实的点击也许因为按钮状态而无法触发onClick事件。

因此,才会有人提出前端的测试应尽量去模拟真实的用户行为,Testing-Library就在其官网的“指导原则”章节,鼓励使用者尽量仿照应用真实的使用方式去编写测试,并明确提出,你的测试越接近用户的真实使用方式,它就能给你越多的信心。换句话说,你的测试应该尽量少用函数去手动触发,而要尽量多地利用测试框架给你的API,去模拟Input框的输入,按钮的点击,表单的提交等等。

如此一来,有的函数,你也无需写测试证明它的返回值如你所愿,需要写的,是页面显示了期待的文字,发生了预期的变化,进行了对应的跳转。你会发现,这时的测试就像写在卡里的AC一样。只要测试是通过的,你就有理由相信主体功能没有破坏,而不只是函数工作正常。

没有独立业务含义的测试单元

看到上面的方案,你可能会立马会想到一些问题。

首先就是测试流程可能会很长,从用户填完表单,点击提交,到期待的变化出现,当中可能经历了好几个函数的执行,连带着一系列的副作用。模拟这一系列行为,似乎是集成测试与E2E测试该干的事情。如果项目中大部分逻辑都是由这种测试去覆盖,看起来与测试金字塔所说的由单元测试作为地基是矛盾的。

我认为,当真实遇到的问题碰到了某种教条规范时,后者该适当地让步。

鼓励多写单元测试的原因在于它们成本低,有针对性。可是在前端项目里面,很多形式上的单元并没有独立的业务含义。

拿React项目举例,好多函数只是因为它们在形式上可以被抽取出来,就被拎到一个单独的文件里,从而降低主函数的复杂度。如果给它写单元测试,你就不得不手动触发它的参数变化,或者检测它的参数函数是否有被调用。

我们写的React hook尤其如此。很多时候抽取自定义的hook是出于逻辑上的原因,把相关的逻辑和数据聚合到一起,减轻UI组件的负担,但这些hook往往没有一个可以轻易解释清楚的业务含义,而且它们也不会被其它地方使用。

所以这类 “单元”只是长得像单元而已,它们其实只是一个实现环节。这里完整的UI操作流程,才更像一个有价值的单元,尽管它们在形式上可能超越了单个函数的范畴。

但我不想矫枉过正,确实有不少情况下,一个util函数,一个hook,一个很小的公共组件,都是有独立存在的价值的,因此,它们也应当被视为真正的单元,确实“有资格”拥有自己的专属测试。

testing-library下面有一个单独的库,叫react-hooks-testing-library,让你无需通过UI行为层面,而是直接以hook的方式去测试它们。它的GitHub页面上,明确提出了使用以及不使用它的场景:当你的hook不与组件强相关,拥有独立含义时可以使用;当你的hook只被一个组件使用,且和它的定义强相关时,则不建议使用。

【插入一段: 尽管存在react-hooks-testing-library这样的工具,但像SWR这样优秀的三方库,在用testing-library为自己的hook API做测试的时候,依然选择在UI层面进行。方法是,把自己的hook置于一个临时的div标签里进行render,把数据的变化映射成html文字的变化,最后对文字内容做断言。其实对于独立性强的函数,个人觉得放置在UI里面做测试倒没有太大区别,但SWR的例子体现了对“仿照真实使用场景去测试”这一原则的尊重。】

将上面的规律套用到Angular项目中,也是类似的。对于独立性和通用性不强的pipe,directive,reducer,effect,service,都可以认为它们是实现流程的一部分,从UI行为层面写好测试即可。

总之,在构思前端测试的时候,与其死守“单元测试”的字面含义,不如结合实际场景,重新思考什么才是真正有价值的“单元”,因地制宜地去写。换种角度表述,与其在意我们写的测试是不是“单元测试”,不如追求更核心的东西——我们的测试有没有以合适的方式去校验逻辑。

另外,当我们的“单元”过大,一些逻辑可能就会覆盖不上。像sonar这类工具,不仅会检查你的行数覆盖率,还会检查你的各项条件语句是否有被测试执行。当一套测试的行为流程囊括了多个函数,而且每个函数都有好几个if…else语句时,想要在UI操作与mock数据上把所有情况都覆盖到,成本就会变得非常高昂。

对于此,我们得承认,无论用什么方式组织测试,覆盖所有的条件分支都是不太现实的,而且价值也不大。对于“满足条件A就执行XXX”之类的语句,条件为非A时没有业务上的规定,如果为了刻意覆盖函数的所有条件,就强行测它在非A的情况下返回一个undefined,则没有太多价值。对这类情况,用UI行为测试主要条件即可,如果你实在觉得有重要的逻辑没有被覆盖,不妨回过头来想想,是不是漏掉了某种输入条件,例如特定的用户键入或者特殊的API mock返回值。但是,当有过多的条件分支很难用业务场景去表述和模拟的时候,我们可能需要重新思考代码的实现逻辑是否合理了。

当然,即使按上面这样做,有时候还是会发现要覆盖的条件组合太多,从行为流程上写测试太复杂,这时就不得不做一定的妥协,为那些没有独立性的部分去单独写测试。如果这类测试不太好写,可以参照刚才提到的SWR官方测试用到的技巧,把要测的函数或者是对象放置在一个临时的UI组件下,以最小的成本做UI行为测试。

最后

总结一下上面谈到的几个原则:

  1. 从真实用户的行为流程去测试,往往比测函数本身,能给你带来更多的信心。
  2. 对于没有独立性和通用性的函数或对象,把它们视作实现的一部分,一般没有必要为它们去写单独的测试。不要拘泥于对“单元测试”的字面理解,不要被形式上的规律所束缚。
  3. 不要把测试覆盖率视为太过重要的指标,它的目的还是帮助提升代码的稳定。有的代码没有覆盖也没关系,有的代码值得你覆盖好多遍。毕竟,我们不是为了写测试而写测试。

文/Thoughtworks钟立
原文链接:https://insights.thoughtworks.cn/front-end-testing/
更多精彩洞见,请关注微信公众号Thoughtworks洞见。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容