37

Vue 应用单元测试的策略与实践 05 - 测试奖杯策略

 4 years ago
source link: https://www.tuicool.com/articles/Y7bUJru
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文的目标

  1. Vue 项目中测试收益如何最大化,如何配置高性价比的测试策略,即什么地方最该花力气测试,什么地方又可以暂且放一放?
// Given
一个具备UT基础但找不到着力点的求索之徒:monkey:
// When
当他:walking:阅读本文的Vue应用测试策略部分
// Then
他能够找到测试的重点,重新燃起对UT的热情:fire:
他能够在项目背景下合理配置单元测试的测试策略

单元测试的特点及其位置

前言从 敏捷:团队和企业的高响应力 谈到单元测试,可能有同学会问,高响应力这个事情我认可,也认可快速开发的同时,质量也很重要。但是,为了达到“保障质量”的目的,不一定得通过测试呀,就算需要测试,也不一定得通过单元测试。

这是一个好的问题。为了达到保障质量这个目标,测试当然只是其中一个方式,稳定的自动化部署、集成流水线、良好的代码架构、甚至于团队架构的必要调整等,都是必须跟上的基础设施。自动化测试不是解决质量问题的银弹,多方共同提升才可能起到效果。

即便我们谈自动化测试,也未必全部都是单元测试。我们对自动化测试套件寄予的厚望是,它能帮我们 安全重构已有代码快速回归已有功能保存业务上下文 。测试种类多种多样,为什么我们要重点谈单元测试呢?原因很简单,因为它写起来相对最容易、运行速度最快、反馈效果又最直接。

测试奖杯:trophy::软件测试的分层策略

测试奖杯( Testing Trophy )是一种 自下而上 的 Web 应用测试策略。其实这是在说我们需要编写_恰到好处的_测试,给予团队足够的信心 —— 正确的测试,而_不是_仅仅追求达到100%的测试覆盖率而已。

3EJZVrB.png!web

使用测试奖杯策略,我们可以将这些自动化测试技术进行分层:

  • 使用静态类型系统和linter 来捕获拼写或语法之类的基本错误。
  • 编写有效单元测试 需要特别针对于应用的某些关键行为或功能。
  • 编写集成测试 以确保 Web 应用各模块之间能够正常协调工作。
  • 创建端到端(e2e)功能测试 对关键路径进行自动化点击操作,而不是等到最终用户来发现问题。

这种四层自动化测试提供了多快好省(放心、快速、省钱)的 JavaScript 专业化测试,最大的特点是它能够反复执行且收益递增,即不需要完全采纳就能获得收益,立马见效。

性价比高的单元测试

对于一个自动化测试策略,应该包含种类不同、关注点不同的测试,比如关注单元的单元测试、关注集成和契约的集成测试和契约测试、关注业务验收点的端到端测试等。正常来说,我们会受到资源的限制,无法应用所有层级的测试,效果也未必最佳。

因此,我们需要有策略性地根据收益-成本的原则,考虑项目的实际情况和痛点来定制测试策略:比如三方依赖多的项目可以多写些契约测试,业务场景多、复杂或经常回归的场景可以多写些端到端测试,等。但不论如何,整个测试金字塔体系中,你还是应该拥有更多低层次的单元测试,因为它们 成本相对最低,运行速度最快 (通常是毫秒级别),而对单元的保护价值相对更大。

单元测试的 F.I.R.S.T 原则

编写容易维护的单元测试有一些原则,这些原则对于任何语言、任何层级的测试都适用。这些原则不是新东西,但总是需要时时温故知新,前人总结成 F.I.R.S.T 五个原则,以此为镜,可以时时检验你的单元测试是否高效:

  • F Fast:测试需要频繁运行,因此要能快速运行;
  • I Independent:测试应该相互独立,一次只测一条分支;
  • R Repeatable:测试本身不包含逻辑,能在任何环境中重复;
  • S Self-validating:只关注输入输出,不关注内部实现;
  • T Timely:测试应该及时编写,表达力极强,易于阅读;

Fast:运行速度快,频繁运行

单元测试只有在毫秒级别内完成,开发者才会愿意频繁地运行它,将其作为快速反馈的手段也才能成立。那么为了使单元测试更快,我们需要:

  • 尽可能地避免依赖。除了恰当设计好对象,关于避免依赖我已知有两种不同的看法:
    • 使用mock适当隔离掉三方的依赖(如数据库、网络、文件等)
    • 避免mock,换用更快速的数据库、启动轻量级服务器、重点测试文件内容等来迂回
  • 将依赖、集成等耗时、依赖三方返回的地方放到更高层级的测试中,有策略性地去做

Independent:一次只测一条分支

通常来说,一条分支就是一个业务场景,是做任务分解(Tasking)过程的一个细粒度的task。为什么测试只测一条分支呢?很显然,如此你才能给它一个好的描述,这个测试才能保护这个特定的业务场景,挂了的时候能给你细致到输入输出级别的业务反馈。

常见的反模式是,实现本身就做了太多的事情,不符合 单一功能(SRP)原则 。如果你发现某个模块的单元测试特别难写的话,那么这个模块的实现本身或输入/输出就足够繁琐,应当作为一种某味道识别出来进行重构。

eu6zIfi.png!web

Repeatable:测试不包含逻辑

跟写声明式的代码一样的道理,测试需要都是简单的声明:准备数据、调用函数、断言,让人一眼就明白这个测试在测什么。如果含有逻辑,你读的时候就要多花时间理解;一旦测试挂掉,你咋知道是实现挂了还是测试本身就挂了呢?特别是对于一些时间或者随机数相关的测试,一定不能够从测试中随机生成这样的测试数据,保证测试中不包含任何过多的逻辑。

但对于一些项目中的 utils 来说,我们期望 util 都是纯函数,即是不依赖外部状态、不改变参数值、不维护内部状态的函数。由于多是数据驱动,一个输入对应一个输出,并且不需要准备任何依赖,这使得它多了一种测试的选择,也即是参数化测试的方式。

参数化测试可以提升数据准备效率,同时依然能保持详细的用例信息、错误提示等优点。jest 从 23 后就内置了对参数化测试的支持,如下:

test.each([
  [['0', '99'], 0.99, '(整数部分为0时也应返回)'],
  [['5', '00'], 5, '(小数部分不足时应该补0)'],
  [['5', '10'], 5.1, '(小数部分不足时应该补0)'],
  [['4', '38'], 4.38, '(小数部分不足时应该补0)'],
  [['4', '99'], 4.994, '(超过默认2位的小数的直接截断,不四舍五入)'],
  [['4', '99'], 4.995, '(超过默认2位的小数的直接截断,不四舍五入)'],
  [['4', '99'], 4.996, '(超过默认2位的小数的直接截断,不四舍五入)'],
  [['-0', '50'], -0.5, '(整数部分为负数时应该保留负号)'],
])(
  'should return %s when number is %s (%s)',
  (expected, input, description) => {
    expect(truncateAndPadTrailingZeros(input)).toEqual(expected)
  }
)

Rn6BBbu.png!web

当然,对纯数据驱动的测试,也有一些不同的看法,认为这样可能丢失一些描述业务场景的测试描述。所以这种方式还主要看项目组的接受度。

Self-validating:只关注输入输出,不关注内部实现

比如购物车“计算总价格”这样的一个功能,测试本身不关注内部实现:你可以用 reduce 实现,也可以自己写 for 循环实现。 只要测试输入没有变,输出就不应该变。这个特性,是测试支撑重构的基础 。因为重构指的是,在不改变软件外部可观测行为的基础上,调整软件内部的实现。

另外,还有一些测试实现代码的执行次序。这也是一种“关注内部实现”的测试,这就使得除了输入输出外,还有“执行次序”这个因素可能使测试挂掉。显然,这样的测试也不利于重构的开展。

此外,对外部依赖采取mock策略,同样是某种程度上的“关注内部实现”,因为mock的失败同样将导致测试的失败,而非真正业务场景的失败。对待mock的态度,肖鹏有篇文章 Mock的七宗罪 对此展开了详细描述,应当谨慎使用。

Timely:表达力极强,易于阅读

测试应该及时编写,只有在当下最熟悉业务的时候,才能够写出表达力最强的测试。而当我们在未来不小心破坏某个功能时,表达力强的测试才能在失败的时候给你非常迅速的反馈。它讲的是两方面:

  • 看到测试时,你就知道它测的业务点是啥
  • 测试挂掉时,能清楚地知道失败的业务场景、期望数据与实际输出的差异

总结起来,这些表达力主要体现在以下的方面:

  • 测试描述 。遵循上一条原则(一个单元测试只测一个分支)的情况下,描述通常能写出一个相当详细的业务场景。这为测试的读者提供了极佳的业务上下文
  • 测试数据准备 。无关的测试数据(比如对象中的很多无关字段)不应该写出来,应只准备能体现测试业务的最小数据
  • 输出报告 。选用断言工具时,应注意除了要提供测试结果,还要能准确提供“期望值”与“实际值”的差异

上述第三点有些测试框架提供了反例,比如说chai和sinon提供的断言API就不如jest友好,体现在:

  • expect(array).to.eql(array) 出错的时候,只能报告说 expect [Array (42)] to equal [Array (42)] ,具体是哪个数据不匹配,根本没报告
  • expect(sinonStub.calledWith(args)).to.be.true 出错的时候,会报告说 expect false to be true 。废话,我还不知道挂了么,但是那个stub究竟被什么参数调用则没有报告

总结一下

“测试需要花费太多时间和精力。”

  • 没时间。 我知道,你已经很忙了。
  • 没有明显的投资回报率。 我知道,你不确定测试到底能带来什么。
  • 没有_办法_测试一切。 我知道,大多数测试都是所谓的_点点点……_。这感觉就像浪费时间,我们都喜欢开发新功能,而不只是对着旧功能“点点点……”。

事实上, 没有人有时间 。但是,无论如何:

你所开发的软件 终将 被测试。如果不是由你自己发现,那么就是由你的用户发现(:boom:Bug)。

「懒惰」是程序员最大的美德

Perl语言的发明人Larry Wall说,好的程序员有3种美德: 懒惰、急躁和傲慢(Laziness, Impatience and hubris)。

懒惰:是这样一种品质,它使得你花大力气去避免消耗过多的精力。它敦促你写出节省体力的程序,同时别人也能利用它们。为此你会写出完善的测试或文档,以免别人问你太多问题。

想象一下,将测试软件的繁重工作全部外包给机器。

你是开发工程师呀,这个时代最伟大的脑力工作者啊!你知道人类在处理重复性任务的时候都很糟糕,但是你还知道 _机器_非常非常擅长复杂的重复性任务。 更专业的开发人员就是会使用计算机来做自动化测试 —— 一整天都在绵绵不休地进行,帮你处理这些测试软件的繁重工作。

  • 自动化测试是专业的。
  • 自动化测试是你的后盾,是你的肌肉。
  • 自动化测试是你的秘密武器……

时不时,问一下自己这几个问题:

  • ,还可以如何偷懒?
  • 应该让计算机帮忙测点什么?
  • 计算机该在什么时候进行测试?
  • 需要100%的覆盖率吗?
  • 多少次测试就足够了?

未完待续……

## 单元测试基础

  • ### 单元测试与自动化的意义
  • ### 为什么选择 Jest
  • ### Jest 的基本用法
  • ### 该如何测试异步代码?

## Vue 单元测试

  • ### Vue 组件的渲染方式
  • ### Wrapper find() 方法与选择器
  • ### UI 组件交互行为的测试

## Vuex 单元测试

  • ### CQRS 与 Redux-like 架构
  • ### 如何对 Vuex 进行单元测试
  • ### Vue组件和Vuex store的交互

## Vue 应用测试策略

## Vue 单元测试的落地


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK