前端单元测试

前端单元测试

首先看一下产品研发整个流程,从图中可以看出,测试是非常重要的环节,可以分为单元测试、集成测试、系统测试、验收测试。我们作为开发,其实不应该仅仅参与开发这个过程,还包括了单元设计、单元测试。后面会详细介绍单元测试。

什么是单元测试

计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。——维基百科

我觉得这个概念已经说的很详细了,就是开发过程中模块、功能、函数等最小单位的测试。每个测试应该是测试最简单的一个函数或功能。

上图中的集成测试,就是在单元测试的基础上,将所有模块按照设计要求组装成为子系统或系统,进行集成测试。

单元测试的意义

成为一名优秀的程序员!

一名优秀的程序员,一定是能够写好单元测试的。虽然开始很痛苦,但是最终我们的收货将是开发质量大大提升了,自信心增强了,bug 少了,技术提升了,头发掉的少了……

  • 减轻开发者负担

    • 提前澄清需求:先写测试可以帮助我们去思考需求,并提前澄清需求细节,而不是代码写到一半才发现不明确的需求
    • 开发流程更流畅:明确的流程,每次只关注一个点,思维负担更小
    • 调试器中花费更少的时间
  • 健壮的代码

    • 代码设计更好
    • 代码更加灵活、可扩展性更高
    • 代码简洁、易读、逻辑性强
    • 代码松散耦合
  • 保护网

    • 快速反馈,早期发现错误,快速定位错误,降低修复 bug 的成本
    • 适应需求变更,支持新功能的可持续发展,可以安全、放心地重构

最后面会带大家在实际操作中感受。

单元测试分类

  • TDD—测试驱动开发,侧重点偏向开发,通过测试用例来规范约束开发者编写出质量更高、bug 更少的代码。先写测试用例,再写功能模块。

(英语:Test-driven development,缩写为TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发始于 20 世纪 90 年代。测试驱动开发的目的是取得快速反馈并使用“illustrate the main line”方法来构建程序。——维基百科

  • BDD — 行为驱动开发,由外到内的开发方式,从外部定义业务成果,再深入到能实现这些成果,每个成果会转化成为相应的包含验收标准。先写主功能模块,再写测试用例。

行为驱动开发(英语:Behavior-driven development,缩写BDD)是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA 和非技术人员或商业参与者之间的协作。BDD 最初是由 Dan North 在 2003 年命名[1],它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。——维基百科

TDD—测试驱动开发

是不是有人第一反应会认为是测试人员驱动开发人员?实际上,TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。

狭义的 TDD:就是单元测试驱动开发 UTDD(Unit Test Driven Development)

广义的 TDD:是验收测试驱动开发 ATDD(Acceptance Test Driven Development),包括行为驱动开发 BDD(Behavior Driven Development)和消费者驱动契约开发 Consumer-Driven Contracts Development 等

TDD 的三层含义

  • Test-Driven Development,测试驱动开发
  • Task-Driven Development,任务驱动开发,要对问题进行分析并进行任务分解
  • Test-Driven Design,测试保护下的设计改善。TDD 并不能直接提高设计能力,它只是给你更多机会和保障去改善设计

TDD 的流程

TDD 的基本流程

红:写一个失败的测试,它是对一个最小单位的需求描述,只关心输入输出,不考虑如何实现
绿:专注在用最快的方式实现当前这个小需求,不管其他需求,也不管代码质量如何
重构:既不用思考需求,也没有实现的压力,只需要找出代码中的坏味道,并用一个手法消除它,让代码变整洁

  1. 创建一个失败的测试
  2. 写出恰好能使测试通过的代码
  3. 重构刚刚实现的代码
  4. 重复前三步

下面的一张图可以清楚的明白编写单元测试的流程:

在我们现在的编码过程中是需要不断调试,不断试错,并且不能保证代码是简洁的。而“红-绿-重构”这种方式是先用脏乱代码表达出来,测试通过之后立刻重构刚写的代码,这是一个持续的循环过程,不能是写了很多实现代码后才开始重构,应该是随时重构你刚刚写出的代码,当你完成这个功能的时候,你的代码就是简洁可用的。

重构是改善代码结构的一种实践,但重构并不会改变由测试定义的行为。

重构不应该是单独拿出来花时间做的一件事情,也不应该出现在项目的计划中。重构应该是日常开发中时时刻刻都在进行的活动,它就是开发活动中不可分割的一部分。

重构应该是在不破坏任何测试的前提下对命名、类、函数和表达式进行修改。在不影响行为逻辑的情况下改善系统的结构。

通过以上,我们可以看出重构是需要完备的测试做安全网,就是这层安全网给了我们重构的信心和勇气。

简单设计原则

优先级从上至下降低

  • 通过测试-最简单的方式让测试通过
  • 揭示意图-表明代码意图
  • 消除重复-去除重复代码
  • 最少元素-使用最少的代码完成这个功能

测试条件格式

在我们编写测试用例的时候通常遵循以下形式:

  • Given-给定上下文
  • When-条件、行为,触发一个动作或者事件
  • Then-对期望结果的验证

传统编码方式 对比 TDD 编码方式

传统编码方式

需求分析 -> 确认需求细节 -> 开发 -> 调试 -> (加需求 -> 开发 -> 调试) ->

QA 测试 -> 提出 bug -> 改 bug、打补丁 -> QA 测试 -> 完成

最终的代码冗余、逻辑混乱,稍微动一下,就可能有未知的错误出现,改了之后还要 QA 测试,然后加班继续改…

TDD 编码方式

  1. 先分解任务-分离关注点
  2. 列 Example-用实例化需求,澄清需求细节
  3. 写测试-只关注需求,程序的输入输出,不关心中间过程
  4. 写实现-不考虑别的需求,用最简单的方式满足当前这个小需求即可
  5. 重构-用手法消除代码里的坏味道
  6. 重复 3、4、5 步骤
  7. 写完功能-手动测试一下,基本没什么问题,有问题补个用例,修复
  8. 转测试-小问题,补用例,修复
  9. 代码整洁且用例齐全,信心满满地交付代码

总结

从上面两个流程不难看出,测试驱动开发最大的优点就是重构了,不断迭代,不断地对现有代码进行重构,不断优化代码的内部结构,最终实现对整体代码的改进。以此不断减少一些设计冗余、代码冗余、逻辑复杂度等等。

缺点就是存在局限性,它不能发现集成错误、性能问题、或者其他系统级别的问题。还要求一定是好的测试用例,如果测试代码太复杂,那么测试代码本身就可能有 bug。

编写测试用例

原则

  • 明确自己要测什么
  • 简单,只测试一个需求,要写刚好的实现
  • 符合 Given-When-Then 格式
  • 速度快
  • 包含断言
  • 可以重复执行
  • 及时重构

粒度设计

  • 大粒度— 测试功能
    • 业务/接口不变的情况下,重构代码,不需要改测试(保证功能不被重构代码改坏)
    • 标题可与需求、QA 测试点对应起来,容易理解
    • 保证各个部件配合起来不出错
    • 干扰多,需要 mock 的依赖比较多
    • 运行时间比较长
  • 小粒度—测试文件、类、方法、纯函数、正则表达式……
    • 重构时可能会被改写或删除
    • 依赖较少
    • 速度快

可靠性

  • 让测试在该失败的时候失败,该通过的时候通过
    • 遵循 TDD 流程写测试,上面有详细的介绍
    • 还没实现功能或者还没改好 bug 前,测试必须是失败的
    • 尝试坏功能,去检测是否失败,如果功能坏了,测试依然通过,那么测试本身是不可靠的
  • 测试的输入输出应该是稳定的,任何时候任何环境下都一样
    • 测试中避免依赖真实的时间、随机生成器、真实的环境(数据库、网络),换句话说,排除掉一切不确定因素的干扰
  • 测试隔离,测试之间不能相互依赖或相互影响
    • 测试之间应该是无序的
    • 测试之间不能共享同一个内存状态
    • 善于使用 afterEach,beforeEach 来重置状态
    • TDD 过程中,给当前测试点加 only,完成后去掉 only,结果都是一样的

可维护性

  • 避免直接测试私有方法/属性
    • 私有方法再重构的时候后往往会被合并、拆分、删除,如果这些操作需要更改对应的单元测试,会增加很多的工作量
  • 减少重复代码
    • 善于使用 describe、beforeEach、beforeAll
    • 提取变量,创建工具方法,setup functions mock utils
  • 测试代码的复杂度应该低于对应的功能实现
    • 适当保留重复,测试代码本身不宜又过多的抽象和逻辑
  • 善于使用测试框架提供的断言 API,提高可读性

单元测试适用场景

明确解决问题方案的前提

TDD 并不是任何时候都适用的,仅限于确定了问题的解决方案后,你可以使用 TDD 来实现一个更优雅的版本。如果是为了探查或者解决某种不确定的问题而要快速实现一些功能时,则会忽略测试代码而直接完成功能代码。

  • 类和函数

一般在开发过程中,可能会自定义一些通用的工具函数或者类,这些都是需要进行单元测试的。

  • 基础组件

一般开发基础组件的时候需要对组件的结构、事件编写单元测试。

单元测试推动过程

单元测试需要足够专业技能、专业素质的人才来保证整个过程的通畅与专业,前期需要一定的投入,介于质量、效率、成本等问题,推动单元测试一定是痛苦的,而且前提还得是领导能够给出单元测试的时间,如果时间依然紧迫还要求写单元测试,那么最终的结局是写了很多无用的测试代码,为了追求测试通过率而不写断言等等问题。

  • 会写测试用例

    每个人熟悉测试框架和编写测试用例流程,且正常编写出测试用例

  • 写对测试用例

    正确的使用生命周期函数、断言库等方法,并且知道哪些该测哪些不用测,关注可测性

  • 写好测试用例

    测试用例尽可能的覆盖全面、不冗余,且是有效的

  • TDD 方式编写测试用例 也许很难

    真正理解 TDD 的理念,认可并运用

前端测试框架

简单了解下几种测试框架

  • Karma - 基于 Node.js 的 JavaScript 测试执行过程管理工具(Test Runner),让你的代码自动在多个浏览器(chrome,firefox,ie 等)环境下运行
  • Mocha - Mocha 是一个测试框架,在 vue-cli 中配合 chai 断言库实现单元测试( Mocha+chai )
  • Jest - Jest 是 Facebook 开发的一款 JavaScript 测试框架。在 Facebook 内部广泛用来测试各种 JavaScript 代码

选择框架

我们选择使用 Jest 框架

  • 配置简单
  • 学习成本低
  • 自身包含了驱动、断言库、mock 、代码覆盖率等多种功能

断言库

断言指的是一些布尔表达式,在程序中的某个特定点该表达式值为真,判断代码的实际执行结果与预期结果是否一致,而断言库则是将常用的方法封装起来

主流的断言库

  • assert (TDD)
1
assert('mike' == user.name);
  • expect.js(BDD) - expect() 风格的断言
1
expect(foo).to.be('aa');
  • should.js - BDD(行为驱动开发)风格贯穿始终
1
foo.should.be('aa');
  • chai(BDD/TDD) - 集成了 expect()、assert()和 should 风格的断言

实际操作

现在我们按照测试驱动开发中 红-绿-重构 的方式完成下述示例。

需求:实现一个检查一串字符串中()、[]、{}几种括号是否闭合

任务分解(第一种 不考虑穿插括号)

计算左括号字符在字符串中出现的个数

计算所有左括号字符在字符串中的索引和

计算右括号字符在字符串中出现的个数

计算所有右括号字符在字符串中的索引和

比较左右索引的大小 左的索引总和小于右的索引就是闭合的 否则是不闭合的

比较左右个数 相同就是闭合的 不同就是不闭合的

举例

目标字符串 期待结果
‘’ true
() true
( false
[] true
] false
()] false
()[]{} true
)([]{} false
({[]}) true

任务分解(第二种 考虑穿插括号)

创建括号字符左右匹配对象

遍历括号数组,遇到任何左括号就入栈

直到遇到右括号,与左括号栈顶字符匹配

能够匹配上继续,并将栈顶符号出栈

匹配失败则跳出循环

举例

目标字符串 期待结果
‘’ true
[({})] true
{()[]} true
)()[] false
({[}]) false
[}{] false

参考文章

深度解读 - TDD(测试驱动开发)

作者

张金秀

发布于

2021-04-06

更新于

2023-08-05

许可协议

CC BY-NC-SA 4.0

评论