公司最大的内卷,偷偷做单元测试
原创- 2024-08-13 10:00:00
- 1223
本篇目录
一位读者在看过我的《理解这八大优势,才算精通单元测试》后,问我:知道单元测试有好处,但实在没空写。看完文章后又想重新落实一下,有没有啥写好单元测试的技巧?
这位读者绝对不是第一个和我抱怨单元测试的人。这很好理解,中国互联网公司太多太卷,想要抢夺市场就要推出不同功能,而这些压力一部分落在了程序员身上,拼命赶需求。单元测试这种费力不讨好的事情,自然而然就没有人做。
就我多年的经验来看,写单元测试其实不会拖延项目,反而能够加快功能研发进度。单元测试的好处我就不在这里赘述了,只有真正尝试过的人才能理解。
马克·吐温曾说:“取得成功的秘诀就是开始”。本篇文章想和大家分享一下写好单元测试的技巧,希望可以给大家带来新方向。
一、单元测试的注意事项
单元测试是为了让我们快速查找并隔离损坏的代码片段。正因如此,这些函数和类在测试时不应该依赖于mock(模拟)和stub(存根)以外的其他元素。在测试中,如果试图覆盖的逻辑过于复杂,就难以确保覆盖的可靠性,也难以准确找出失败的原因。
因此,我们要注意单元测试包括以下几点。
01 简洁性
短函数更容易阅读和理解。我们每次只测试一个逻辑点,因此测试代码应该控制在几行之内。但如果是高级逻辑可能具有多个依赖项,这就需要大量样板代码来初始化模拟和存根。此外,单元测试同样适用DRY原则(Don’t repeat yourself,一次且仅一次),我们在写单元测试时要避免到处复制粘贴混乱的代码,最好使用组合而不是继承。
02 明确性
单元测试要使用详尽的长名称。这样的名称不仅能清楚表达信息,还能起到索引作用、快速定位相应测试。就算需求发生变化,我们只需要针对相应的测试进行更改,不必查看所有内容并检查受影响的内容。
好的单元测试一般只有一个断言,因此命名起来也很容易。例如,在处理金额计算时,it('should return 0 for an empty cart')
要比it('works for 0')
或者 it('empty cart')
好得多。对于使用函数名称作为测试名称的框架也是如此,shouldReturnZeroForAnEmptyCart
就是一个很不错的的命名。
正如丁玲所言:“人生就像爬坡,要一步一步来。”单元测试也是如此,不要一次性测试整个方法,要一步一步来。我们只针对单个需求写单元测试,代码就会变得易于阅读和维护。
03 可维护性
测试框架需要提供各种断言方法。它们提供不同的方法来检查结果,并且当断言失败时,它们还会显示更具体的错误消息,从而提供更多上下文来查看错误所在。
例如,
expect(result === expected).toBeTruthy();
将会失败
expect(received).toBeTruthy() Received: false
尽管
expect(result).toBe(expected);
将提供更多有关具体失败原因的信息:
expect(received).toBe(expected) // Object.is equality Expected: "John Doe" Received: "JohnDoe"
框架还为不同的测试方式提供了各种断言。例如,在使用Jest进行测试时,toBe
使用Object.is
测试是否完全相等,而toEqual
和toStrictEqual
则深入比较对象,确保他们的类型和结构一致。
toBeCloseTo
。虽然toEqual
有时也能适用,但即使是看似简单的测试,如expect(0.1+0.2).toEqual(0.3)
也可能无法通过。二、单元测试的AAA原则
遵循AAA原则(Arrange、Act、Assert,安排、执行、断言),可以娴熟提升单元测试代码的清晰度、可靠性和可维护性。
第一步,安排阶段(Arrange)。我们需要完成变量赋值、对象实例化对象以及测试运行所需的其余前置设置,并且定义预期结果。这样做的好处在于:一方面,我们需要在执行测试逻辑前就有明确预期;另一方面,这更方便在输入数据后立即查看预期输出,有助于避免代码混淆。
第二步,执行阶段(Act)。我们将执行测试函数并存储其结果。结果存储其实是准备工作的自然延伸,有助于我们对结果进行回顾总结。
第三步,断言阶段(Assert)。我们在这个阶段可以判断假设的正确性了。这正是单元测试的核心所在,因为这一环节实际上是对某些具体内容的测试。其目的在于是检查实际得到的结果否与预期结果相匹配。
我们要确保代码可靠性,避免错误输入、缺少参数、空数据、调用函数中的异常等情况的出现。代码覆盖率工具可以帮助我们查漏补缺,找到未测试的代码分支。我们要始终明确我们单元测试的目标,过于追求100%测试覆盖率反而会让单元测试代码越来越繁杂。这与《吕氏春秋》中的论点不谋而合:“不知轻重,则重者为轻,轻者为重矣。若此,则每动无不败”。
三、单元测试的优化和维护
为了提高单元测试效率,我们需要模拟所有可能影响速度的外部依赖项,例如API调用、数据库或文件系统访问。我们在写单元测试时,应尽量避免线程休眠、等待和超时。如果必须设置超时,就应该将其缩短至几毫秒。在处理多线程或异步竞争条件时,精确控制出发条件比简单的等待要有效得多。
单元测试应当确保不会改变作用域外的任何内容。如果测试仅在按照特定顺序执行时才能成功,这可能表明测试用例或测试代码存在问题。每个测试用例应独立运作。由于现代测试框架默认并行执行测试,因此我们不应依赖全局变量或之前测试的遗留效应。这也是全局变量常被视为不良编程习惯的原因之一,这会隐藏真正的依赖关系,导致代码耦合度升高,并在处理多线程问题时需要格外留意。
当测试需要复杂的重复配置时,应利用框架提供的设置和清理功能。这些功能保障了在每个测试用例或整个测试套件开始前后,相关代码能够得到执行。这样,无论是单独运行测试还是作为测试套件的一部分,都能确保测试结果的确定性,执行顺序不会对测试结果造成影响。
四、单元测试贵在坚持
《荀子·大略》:“夫尽小者大,积微成著,德至者色泽洽,行尽而声问远。”单元测试的作用只有经过长期积累才会变得显著。其实,写单元测试更多的是对自己的代码负责。有测试用例的代码,别人更容易看懂,以后别人接手你的代码时,也可能放心做改动。