前端单元测试库

软件测试入门

https://bobi.ink/2019/07/10/typeof-testing/

  • 功能测试类型:

    • 单元测试(Unit testing)
    • 集成测试(Integration testing)
    • 系统测试(System testing)
    • 健全性测试(Sanity testing)
    • 冒烟测试(Smoke testing)
    • 接口测试(Interface testing)
    • 回归测试(Regression testing)
    • Beta/验收测试(Beta/Acceptance testing)
  • 非功能测试类型:

    • 性能测试(Performance Testing)
    • 负载测试(Load testing)
    • 压力测试(Stress testing)
    • 容量测试(Volume testing)
    • 安全测试(Security testing)
    • 兼容性测试(Compatibility testing)
    • 安装测试(Install testing)
    • 恢复测试(Recovery testing)
    • 可靠性测试(Reliability testing)
    • 可用性测试(Usability testing)
    • 一致性测试(Compliance testing)
    • 本地化测试(Localization testing)

等价划分

等价划分, 这是一种黑盒测试的测试技术. 通过等价划分,可以将所有的输入数据合理地划分为多个分组,我们只需在每个分组中取一个数据作为测试的输入条件, 这样可以实现用少量代表性的测试数据取得较好的测试结果.

所以说这个测试的目的: 是在不导致缺陷的前提下,移除指定分组中的重复的用例, 简化测试的工作

组件测试

组件测试(此组件非GUI组件, 取组合测试可能更好理解一点),一般也称为模块测试(Module Testing), 一般由开发者在完成单元测试后执行。组件测试将多个功能组合起来作为单一的整体进行测试,目的是发现多个功能在相互连接起来之后的缺陷。

组件测试可大可小,小到函数级别或者类级别的组合,大可以大到几个单独的页面、模块、子系统的组合。 举一个前端例子,将多个页面路由组合起来,测试它们的流程跳转,就属于组件测试。

端到端测试

端到端测试也是一种黑盒测试类型,类似于系统测试. 端到端测试在模拟的、完整的、真实应用环境下模拟真实用户对应用进行测试,比如应用会和数据库交互、会使用网络通信、或者在适当的情况下和其他硬件、应用、系统进行交互. 端到端是指从一个端点到另一个端点的意思,所以端到端测试重点用于测试模块和模块之间的协调性。

当应用是分布式系统或者需要和其他外部系统协同时,端到端测试扮演着非常重要的角色, 它可以全面检查以确保软件在不同平台和环境产品能准确地交互。端到端测试有以下目的:

  • 确保应用可以和外部系统之间良好的协调。对于前端来说,是确保页面和后端之间良好协调
  • 检查从源系统到目标系统的所有系统流
  • 从最终用户角度验证需求
  • 识别异构环境中的问题

前端也有很多自动化的端到端测试工具,比如nightwatch,通过它们可以模拟用户对页面进行操作,从而检验整个应用流程是否正常和符合需求:

功能测试

功能测试是一个大类, 又称为行为测试, 功能测试会忽略内部实现而关注组件的输出,目的是验证是否符合需求,这是一种面向功能需求的黑盒测试类型。关于功能测试的细节请看这里

功能测试是相对非功能测试而言的, 功能测试需要关心功能或者业务,需要业务耦合程度高;而非功能测试则是通用的,比如压力测试、负载测试,这些测试都有通用的工具来支持,不需要或很少定制化操作.

GUI测试

GUI测试的目的是根据业务需求验证GUI。在详细设计文档和GUI模型(UI设计文档)中一般会提到应用期望的GUI.

常见的GUI测试包括测试屏幕上显示的按钮和输入字段的大小、表格中所有文本、表格或内容的对齐规则等等. 如果团队有UI设计规范,还会验证是否符合设计规范

大猩猩测试

大猩猩测试是由测试人员执行的测试类型,有时也由开发人员执行。在大猩猩测试中,对模块中的一个模块或功能进行了彻底和严格的测试。原文没有说出大猩猩测试的精髓,大猩猩测试会对一个功能或模块进行重复‘上百次’的测试, 人类根本受不了这样子的测试方式,所以大猩猩测试的另一个别名是‘令人沮丧的测试(Frustrating Testing)’

这种测试的目的是检查应用程序的稳健性(robustness)

集成测试

集成测试是指将所有模块集成之后,验证合并后的功能. 模块通常是代码模块、单个应用、网络上的客户端和服务器应用等等。

集成测试一般在单元测试之后,所以单元测试是集成测试的基础,没有进行单元测试的集成测试是不靠谱的。所以最简单的形式是:’把两个已经测试过的单元组合成一个组件,测试它们之间的接口’。也就是说集成测试在单元测试的基础之上,将单元测试中独立的单元合并起来,验证它们的协调性, 合并后的组件又是一个新的‘单元’,这样逐步合并测试,最终形成完整的应用程序。

这种类型的测试常用于B/S软件和分布式系统。

乐观路线测试

乐观路线测试的目标是在正常流程上成功测试应用。它不会考虑各种负面或异常情况。重点只关注于验证应用在有效和合法输入的条件下生成期望的输出. 比如银行付款,只考虑账户有钱的正常状态😂

增量集成测试

增量集成测试是一种自下而上的测试方法,即在添加新功能时立即集成应用程序进行连续测试。应用程序功能和模块应该足够独立,以便单独测试。这通常由程序员或测试人员完成。

安装卸载测试

安装和卸载测试是在不同硬件或软件环境下的不同操作系统上的进行完整/部分的安装、升级、卸载、回滚等测试. 常用于桌面端应用

探索测试

探索性测试有点类似于Ad-Hoc测试. 探索性测试是由测试团队进行的非正式测试。此测试的目的是探索应用并查找应用中存在的缺陷。像探险一样,在测试期间是有一定几率发现的重大、甚至可能导致系统故障的缺陷.

在探索性测试期间,建议跟踪记录好测试的流程、以及开始该流程之前的活动记录, 方便复现bug.

探索测试不需要任何文档和测试用例.

Alpha测试

Alpha测试这是软件工程中很常见的测试类型。它的目标就是尽可能地在发布到市场或交付给用户之前找出所有的问题和缺陷。

Alpha测试一般在开发的末段且在Beta测试之前进行。在这个测试过程中可能会驱动开发者进行一些小(minor)的设计变动. Alpha测试一般在开发者网站进行,即只对开发者或内部用户开放,一般可以为此类测试创建内部虚拟的用户环境。

Beta测试

上文Alpha测试已经提及Beta测试, Beta测试是一种正式的软件测试类型,在将产品发布到市场或者实际最终用户之前,由客户在真实的应用环境中执行。

执行Beta测试目的是确保软件或产品中没有重大故障,并且满足最终用户的业务需求。当客户接受软件时,Beta测试才算通过。

通常,此类测试由最终用户或其他人完成。这是在将应用发布作为商业用途之前完成的最终测试。通常,发布的软件或产品的Beta版本仅限于特定区域中的特定数量的用户。 所以最终用户实际使用软件后会将一些问题反馈给公司。公司可以在全面发布之前采取必要的措施。

Beta测试在正式版本之前也可能会迭代进行多次.

A/B测试

顾名思义, A/B测试就是准备两个(A/B)或两个以上的版本,让不同的用户来随机访问这些版本,收集各群组的用户体验数据和业务数据,最后分析、评估出最好版本,正式采用。如上图,谷歌使用A/B测试来决定导航应该是红色还是蓝色。

验收测试(Acceptance Testing)

验收测试通常是部署软件之前的最后一个测试操作, 也称为交付测试, 由最终客户执行,他们会验证端到端(end to end)的系统流程是否符合业务需求,以及功能是否是满足最终用户的需求。只有当所有的特性和功能按照期望的运行,客户才会接受软件

这是测试的最后阶段,在验收测试之后,软件将投入生产环境. 所以它也叫用户验收测试(UAT)

临时测试

Ad-hoc中文应该理解为临时的意思。顾名思义,这种测试是在临时基础上进行的, 有时候也称为随机测试。即没有参考测试用例、没有针对该测试的任何计划和文档。Ad-hoc测试的目的就是通过执行随意的流程或任意的功能来找出应用的缺陷和问题

Ad-hoc测试一种非正式的方法,可以由项目中的任何人执行。尽管没有测试用例很难识别缺陷,但是有些时候在Ad-hoc测试期间发现的缺陷可能无法使用现有的测试用例来识别, 也就是说它一般用来发现‘意外’的缺陷.

可访问性测试

可访问性测试的目的是确定软件或应用程序是否可供残疾人使用。残疾是指聋人,色盲,智障人士,失明者,老年人和其他残疾人群体。这里会执行各种检查,例如针对视觉残疾的字体大小测试,针对色盲的颜色和对比度测试等等。

不同平台、不同应用类型对可访问性支持情况不太一样,比如iOS相比其他操作系统则更重视可访问, 而国外比国内更重视可访问性。

后端测试

前端应用输入的数据,一般都会存储在数据库,所以针对数据库的这类测试称为数据库测试或者后端测试. 市面有不同的数据库,如SQL Server,MySQL和Oracle等。数据库测试会涉及表结构,模式,存储过程,数据结构等。

后端测试一般不会涉及GUI,测试人员通过某些手段直接连接到数据库,从而可以容易地运行一些数据库请求来验证数据。通过后端测试可以发现一些数据库问题,比如数据丢失、死锁、数据损坏。这些问题在系统投入生产环境之前进行修复至关重要

浏览器兼容测试

这是兼容性测试的子类型,由测试团队执行. 浏览器兼容测试主要针对Web应用,用于确保软件可以在不同浏览器或操作系统中运行; 或者验证Web应用程序是否支持在浏览器的所有版本上运行, 以确定应用最终兼容的范围.

浏览器兼容测试是前端开发者绕不开的坑。

我们有很多策略来应对浏览器兼容性,比如渐进增强或者优雅降级, 还有制定浏览器兼容规范;

为了抚平浏览器之间的差异,我们会使用各种特性检测工具(Modernizr), 还有各种polyfill(CSS Normaliz, polyfill/shim, css-autoprefixer);

当然为了测试跨浏览器兼容性,还要一些辅助工具,例如BrowserStack, 对于我们这些小团队,只能下一堆Portable(Portable浏览器运行时相互隔离的, 所以不会存在配置文件等冲突问题) 浏览器,手工测试了。

向后兼容测试

向后兼容测试, 用于验证新开发或更新的软件是否能在旧版本的环境中运行。

比如向后兼容测试会检查新版软件是否可以正确地处理旧版本软件创建的文件格式。例如新版的Office 2016是否可以打开2012创建的文件。

同理也可以检查新版本是否可以兼容旧版本软件创建的数据表、数据文件、数据结构、配置文件。

任何软件更新应该在先前版本的基础之上良好地运行

黑盒测试

黑盒测试不考虑软件的内部系统设计,它基于需求和功能进行测试, 只关心系统的输入/输出以及功能流程。

换句话说黑盒测试从用户的角度出发针对软件界面、功能及外部结构进行测试,而不考虑程序内部逻辑结构.

黑盒测试下面有很多子类,例如集成测试、系统测试、大部分非功能性测试

边界值测试

边界值测试, 测试应用处于边界条件(boundary level)的行为。很多边界条件开发者是很难考虑周到的,所以才有一个专门的测试类型来验证这种情况

边界值测试检查应用处于边界值时是否存在缺陷。边界值测试通常用于测试不同范围的数字, 每个范围都有一个上下边界,边界测试则是针对这些边界值进行测试。

比如数字范围为1-500, 那么边界值测试会在这些值上进行验证: 0、1、2、499、500、501

分支测试

这是白盒测试的子类型,在单元测试中实施. 顾名思义,分支测试表示测试要覆盖程序代码的各种条件分支, 避免遗漏缺陷。分支覆盖是单元测试覆盖率的一个指标之一

比较测试

比较测试,将产品的优点和弱点与旧版本或者同类(竞品)产品进行比较.

比如类似王自如这种数码测评栏目,评测一个手机或者其他数码产品时,一般会横向和友商产品进行比较,有时候也会纵向和上一代产品比较.

还有一种比较典型的例子就是和行业的领导者比较,比如我们做IM的,会经常和微信比较: ‘你这个应用的启动速度怎么比微信慢这么多?’

前端单元测试框架

前端测试框架有

  • Mocha
  • Jasmine
  • Jest
  • Tape
  • Karma

写好测试

测试的第一法则:瘦测试

测试代码与生产代码不同,要使它变得极其简单、短小、没有抽象、扁平化、使人愉悦、瘦。一段测试代码需要做到让人一眼就能看出其目的。

我们的思维空间被主体生产代码充满,因此无法腾出额外的“大脑空间”存放复杂的东西。如果向可怜的大脑中塞进其他复杂代码,将会使得整个部分变慢,而这个部分正是用来解决我们需要测试的问题的。这也是大部分团队放弃测试的原因。

另一方面,测试是一个友好的助手,一个你乐于与之合作、投资小回汇报大的助手。科学证明我们有两套大脑系统:系统 1 用于无需努力的活动如在一个空旷的路上开车;系统 2 用于复杂和繁琐的工作如算一道数学表达式。将你的测试为系统 1 设计,当你看一段测试代码时,需要像改 HTML 文档一样简单而不是像计算 2 × (17 × 24)。

为了达到这个目的,我们可以通过选择性价比高、投入产出比(ROI)高的技术、工具以及测试对象。仅测试需要的内容,努力保持其灵活性,某些时候甚至值得去舍弃一些测试来换取灵活性和简洁性

测试应该包含的部分

一个测试报告需要让不熟悉代码的人(测试、运维)明确知道新的变更是符合需求。因此测试名称需要从需求层面描述,并且包含三个部分:

(1) 被测的是什么?(比如 ProductsService.addNewProduct 方法)

(2) 在什么条件和场景下?(比如没有 向该方法传入 price 参数)

(3) 期望的结果是什么?(比如不允许添加该产品)

使用AAA模式构造测试内容

将你的测试内容划分为三个部分:布置,执行,断言 —— Arrange, Act & Assert (AAA)。这样读者就无需动用脑细胞理解你的测试内容了:

1st A - 准备(Arrange):一些用于提供上下文的代码。可能包含:构造数据、添加 DB 记录、mocking/stubbing 对象,以及其他的准备代码;

2nd A - 执行(Act):执行测试单元。通常一行代码。

3rd A - 断言(Assert):保证得到的值符合预期。通常一行代码。

用产品的语言描述期望:使用BDD形式的断言

使用声明的方式写代码,可以使读者无脑 get 到重点。而如果你的代码使用各种条件逻辑包裹起来,则会增加读者的理解难度。因此,我们应尽量使用类似人类语言的形式描述如 expect 或 should 而不是自己写代码。如果 Chai 和 Jest 不包含你想要的断言,而且这种断言可被高度复用时,你可以考虑 扩展 Jest 匹配器 (Jest) 或者写一个 自定义 Chai 插件

坚持黑盒测试:只测public方法

测试内部逻辑是无意义且浪费时间的。如果你的 代码/API 返回了正确的结果,你真的需要花三个小时时间去测试它内部究竟如何实现的,并且在之后维护这一堆脆弱的测试吗?每当测试一个公共方法时,其私有实现也会被隐式地测试,只有当存在某个问题(例如错误的输出)时测试才会中断。这种方法也称为行为测试。另一方面,如果你测试内部方法(白盒方法)—你的关注点将从组件的输出结果转移到具体的细节上,如果某天内部逻辑改变了,即使结果依然正确,你也要花精力去维护之前的测试逻辑,这无形中增加了维护成本。

使用正确的测试替身(Test Double):避免总用stub和spy

测试替身是把双刃剑,他们在提供巨大价值的同时,耦合了应用的内部逻辑 (这里有一篇关于测试替身的文章: mocks vs stubs vs spies). 在使用测试替身前,问自己一个很简单的问题:我是用它来测试需求文档中定义的可见的功能或者可能可见的功能吗?如果不是,那就可能是白盒测试了。 举例来说,如果你想测试你的应用程序在支付服务宕机时的合理表现,你可以 stub 支付服务并触发一些“无响应”返回,以确保被测试的单元返回正确的值。这可以测试特定场景下的应用程序的行为、响应、输出结果。你也可以使用一个 spy 来断言当服务宕机时发送了一封电子邮件——这又是一个针对可能出现在需求文档中的行为的检查(“如果无法保存付款,请发送电子邮件”)。反过来,如果你 mock 的支付服务,并确保它被正确调用并传入正确的 JavaScript 类型,那么你的测试重点是内部的逻辑,它与应用的功能关系不大,而且可能会经常变化。

CI

1.丰富你的linter并丢弃有lint问题的构建

只需五分钟配置,即可免费获取自动保护代码的工具来捕获代码中的显著问题。Lint 不再只是样式工具,现在的 linter 可以捕获很多严重的问题比如 error 没有被正确抛出以及信息丢失。在基础 rule(如 ESLint standard 或 Airbnb style)之上,我们可以考虑加入一些特殊的 linter,例如 eslint-plugin-chai-expect 可以发现用例没有写断言,eslint-plugin-promise 可以发现 promise 没有 resolve,eslint-plugin-security 可以发现可能被 DOS 攻击的正则表达式,以及 eslint-plugin-you-dont-need-lodash-underscore 擅长在代码使用 V8 核心方法后给出告警,如 Lodash._map(…)。

2.通过本地的开发CI开缩短反馈循环

在本地使用一个包含测试、Lint、稳定性检查等功能的 CI 可以帮助开发者迅速得到反馈并缩短反馈循环。因为一个有效的测试流程包含很多迭代循环 (1) 尝试 -> (2) 反馈 -> (3) 重构。所以反馈越快,开发者可以在每个模块中可以执行的迭代就越多,并且可以得到更好的结果。反过来,如果反馈来得很慢,则一天只能执行很少的迭代,则团队可能会因急需执行下一个主题/任务/模块 而不再提炼当前模块。

3.在真实的生产环境镜像中执行端到端测试

端到端测试是每个 CI 的主要挑战——实时创建一个生产环境镜像并带上所有相关的云服务是很费时费力的。你需要找到最佳的折中:Docker-compose 通过一个文本文件将独立的 docker 环境放置到相同的容器中,但是背后的技术(如网络、构建模型)与真实世界有所差别。你可以将其与‘AWS Local’结合在真实的 AWS 服务中使用。如果你使用了 serverless 框架, AWS SAM允许本地调用 FaaS 代码。

Kubernetes 强大的生态系统还没有形成一个易用的标准工具用于本地和 CI 镜像,虽然经常推出许多新的工具。一种方法是使用像 Minikube 和 MicroK8s 这样的工具来运行一个“最小化的 kubernetes”,这些工具更接近实际,但是开销更少。另一种方法是在远程的 “真实 Kubernetes” 上进行测试,一些 CI 提供商(例如 Codefresh)与 Kubernetes 环境进行了本地集成,使得在真实环境中运行 CI 管道变得很容易,其他的则允许针对远程 Kubernetes 进行自定义脚本。

4.并行测试工作

只要操作合理,测试是你 7x24 小时的朋友,为你提供非常及时的反馈。实际上,在单个线程上执行 500 个单元测试可能需要很长时间。幸运的是,现代测试运行器和 CI 平台(如 Jest, AVA 和 Mocha extensions)可以将测试并行化为多个进程,以显著缩短反馈时间。 一些CI供应商也支持跨容器并行化测试,这进一步缩短了反馈循环。 无论是在本地多个进程,还是在使用多台机器的某些云 CLI 上 - 并行化需要保证测试用例的独立性,因为每个用例可能在不同的进程上运行。

5.对于依赖:持续检查有漏洞的依赖/自动升级依赖

测试结果反馈

1.保证足够的覆盖率

测试的目的是为了获取足够的自信去快速迭代,显然,越多代码被测试到,则我们团队越自信。覆盖率用于度量多少代码行(以及分支、语句等)被测试执行到。所以多少够了?10-30% 明显无法证明项目的正确性,而 100% 则非常耗时并且可能会使得你关注太多细枝末节的代码。我们的答案是取决于应用的类型——如果你正在建造 A380 的下一代,那么 100% 是必须的;而对于一个漫画网站,50% 可能太多了。尽管大部分测试拥趸们强调覆盖率门槛是依赖所处环境的,但是他们大部分提到 80% 是一个不错的规则(Fowler: “in the upper 80s or 90s”)大概可以满足大部分应用。

你可能想在你的 CI 中设置覆盖率门槛,并阻止不满足要求的构建(也可以为每一个组件设置门槛,见下面的例子)。另外,我们可以监测构建的覆盖率下降(当新提交的代码的覆盖率较低时)——这将推动开发者提升或者至少保持被测试的代码数。说了这么多,覆盖率仅仅是一个可量化的度量值,它并不能确切地证明你的测试的健壮性,你也可能被它骗到

2.检测覆盖率报告,以发现未覆盖的区域和其他奇怪的地方

有些问题隐藏在雷达之下,而使用传统工具很难发现它们。它们通常不是真正的 bug,大多数情况下是应用的怪异表现,而这种表现可能造成严重影响。例如,一些代码区域几乎不会或很少被调用——你以为“PricingCalculator”类只会设置产品价格,结果他几乎不会被调用,即使我们的数据库中有 10000 件商品以及很多交易……代码覆盖率报告可以帮助你发现应用是否按照你的期望执行。初次之外,它高亮了那些类型的代码没有被测试到——80% 的代码被测试并不能说明你的关键部分被覆盖到。生成报告很简单——只需在构造或测试覆盖率时跑你的应用,然后看看花花绿绿的报告来告诉你每一片代码区域被多频繁地调到。如果你花一点时间看看这些数据——你可能会发现一些问题。

3.使用变异测试度量逻辑覆盖率

传统覆盖率通常是骗人的:它可能显示了 100% 的代码覆盖率,但是你的所有的函数都没有返回正确的结果。怎么回事?它只是简单地度量你的测试代码访问过哪些代码行,而不会检查 tc 是否真正地测试了什么——断言了正确的返回。 基于变更的测试适用于这个需求。它度量了真正被测试过的代码而不是仅仅被访问过的。Stryker 是一个用于变异测试的 JavaScript 库,而它的实现很巧妙:

(1) 它有意地改变代码并「植入 bug」。例如代码 newOrder.price===0 会被改成 newOrder.price!=0,这个 “bug”即成为变异。

(2) 它跑一遍用例,如果所有都成功了则说明有问题——这些用例没有真正实现他们发现 bug 的目的,这些变异即所谓的“存活”了。如果用例失败了,那么很棒,变异被杀掉了。

相对于传统覆盖率,得知所有或者大部分变异被杀掉会给予你更高的信心,而两者花费的时间差不多。

4.使用Test linter防止测试代码问题

有一系列 ESLint 插件用于检查测试代码的风格并发现问题。比如 eslint-plugin-mocha 会警告一个写在 global 层的用例(不是 describe() 语句的子级),或者当测试被 skip 时会发出警告,这可能会导致你错误地认为所有测试都通过了。类似的,eslint-plugin-jest 可以在一个用例没有任何断言(不覆盖任何内容)时给出警告。

前端测试

1.将UI与功能分离

当专注于测试组件逻辑时,UI 细节就变成了应该剔除的噪音,这样您的测试就可以集中在纯数据上。实际上,通过抽象从代码中提取所需的数据将降低与图形实现的耦合,仅对纯数据 (vs HTML/CSS 图形细节) 断言,并禁用会拖慢速度的动画。您可能会试图避免渲染,仅测试 UI 后面的部分(例如,服务、操作、存储),但这将导致测试与实际情况不太相符,「正确的数据根本无法到达 UI」这种问题就无法发现。

2.使用不太容易改变的属性去查询HTML元素

通过不太同意受图形变更印象的属性查询 HTML 元素(例如 form label,而不是 CSS selector)。如果指定的元素没有这样的属性,则创建一个专用的测试属性,如“test-id-submit-button”。这样做不仅可以确保您的功能/逻辑测试不会因为外观变化而中断,而且整个团队可以清楚地看到,测试使用了这个元素和属性,不应该删除它

3.尽量使用真实并且完全渲染的组件进行测试

只要大小合适,就像用户那样从外部测试组件,完全渲染 UI,对其进行操作,并断言呈现的 UI 的行为符合预期。避免各种 mock、部分和 shallow render——这么做可能会由于缺乏细节导致未捕获的 bug,并且由于测试与内部的混在一起将增加维护成本(参见小结“多用黑盒测试”)。如果其中一个子组件明显拖慢测试(如动画)或使很难配置,可以考虑主动用伪组件替换它。

综上所述,需要注意的是: 这种技术适用于封装一定数量子组件的中小型组件。如果一个组件包含太多的子组件,那么将很难对失败测试进行定位(分析根本原因),并且可能会变得过于缓慢。在这种情况下,只需针对胖父组件编写少量测试,而针对其子组件编写更多测试。

4.不要sleep,使用框架内置的对 async 事件的支持。并且尝试提效。

在许多情况下,被测试单元的完成时间是未知的 (例如,animation 挂起了元素表现 )——在这种情况下,不要 sleep (例如setTimeout),并是使用大多数框架提供的更靠谱的方法。一些库允许等待操作 (例如 Cypress .request('url')),另一些库提供用于等待的 API,如 @testing-library/dom 方法 wait(expect(element))。有时一种更优雅的方法是 stub 慢的资源,比如API,然后一旦响应时间变得确定,组件就可以显式地重新渲染。当依赖一些 sleep 的外部组件时,加快时钟可能会提供帮助。sleep 是一种需要避免的模式,因为它会迫使您的测试变得缓慢或有风险(当等待的时间太短时)。当 sleep 和轮询不可避免且测试框架原生不支持时,一些npm库 (如 wait-for-expect) 可以帮助解决半确定性问题。

5.观察内容是如何通过网络提供的

使用一些活动监视器,以确保在真实网络下的页面负载是最优的——这包括了一些用户体验问题:如缓慢的页面负载或未压缩的包。检查工具市场很丰富:像 pingdom、AWS CloudWatch、gcp StackDriver 这样的基础工具可以很容易地配置来监视服务器是否处于活动状态,并在合理的 SLA 下响应。不过这只解决了表面上的问题,因此最好选择专门用于前端的工具 (如 lighthouse、pagespeed) 以进行更全面的分析。注意力应该放在症状和直接影响用户体验的指标上,比如页面加载时间、有意义的绘制、页面可交互(TTI) 时间。最重要的是,你还可以关注技术原因,比如确保内容被压缩、第一个字节的时间、优化图像、确保合理的 DOM 大小、SSL 和许多其他方面。建议在开发期间使用这些丰富的监视器,作为 CI 的一部分,最重要的是在生产服务器/CDN上 24x7 使用它们。

7.写几个跨越整个系统的端到端测试

8.通过复用登陆凭证提速E2E测试

在涉及真实的后端并依赖有效的用户 token 进行 API 调用的 E2E 测试中,我们没有必要将测试按照「创建用户并在每个请求中登录」的级别隔离。相反,在测试执行开始之前只登录一次 (即 before-all hook),将 token 保存在一些本地存储中,并在请求之间复用它。这似乎违反了核心测试原则之一——保持测试的自治,不要耦合资源。虽然这是一个合理的担忧,但在 E2E 测试中,性能是一个关键问题,在执行每个用例之前创建 1-3 个 API 请求可能会大大增加执行时间。复用凭证并不意味着测试必须基于相同的用户记录——如果依赖于用户记录 (例如测试用户付款历史记录),那么要确保生成这些记录作为测试的一部分,并避免与其他测试共享它们。还要记住后端是可以 fake 的——如果你想重点测试前端,那么最好隔离它,然后 stub 后端 API

9.创建一个e2e冒烟测试,仅仅走一遍网站地图

为了监控生产环境以及开发时的完整性检查,运行一个 E2E 测试,该测试访问所有或大部分站点页面并确保没有被中断。这种测试投资回报率极高,因为它非常容易编写和维护,但可以检测任何类型的故障,包括功能、网络和部署问题。其他类型的冒烟和完备性检查并没有那么可靠和详尽——一些 ops 团队只是 ping 主页 (生产),或者开发人员运行一些集成测试无法发现打包和浏览器问题。毫无疑问,烟雾测试不会取代功能测试,而只是作为一个快速的烟雾探测器。

10.将测试以实时协作文档的形式公开

除了提高应用程序的可靠性,测试还带来了另一个极具吸引力的场景——作为实时应用文档。由于测试本质上使用的是一种技术含量较低的产品 / UX 语言,因此使用正确的工具可以将他们作为一个沟通媒介,便捷地协调了所有的同事——开发人员和他们的客户。例如,一些框架允许使用人类可读的语言来表达流程和期望 (即测试计划),这样任何相关人员,包括产品经理,都可以阅读、批准和协作测试,这时测试就成为了实时的需求文档。这种技术也被称为“验收测试”,因为它允许客户用简单的语言定义他的验收标准。这是最纯粹的 BDD (行为驱动测试)。支持此功能的流行框架之一是 Cucumber,它具有 JavaScript 风格,参见下面的示例。另一个相似但不同的场景是 StoryBook,它可以将 UI 组件公开为一个图形化的目录,用户可以浏览每个组件的各种状态(如一个栅格组件的 w/o filter,使其渲染多行或者 0 行,等等),查看它的展示形式,以及如何触发状态——这也可以提供给产品人员,但主要是作为实时文档提供给消费这些组件的开发人员。

11.使用自动化工具检测可视化问题

设置自动化工具来抓取 UI 截屏,并在变更后检测内容重叠或中断等可视化问题。这样不仅可以确保数据的正确性,而且用户可以方便地看到它。这种技术没有被广泛采用,我们的测试思维更倾向于功能测试,但它代表了真实的用户体验,而且可以轻易地发现跨多设备类型的 UI bug。目前部分免费工具可以提供一些基础功能——生成和保存屏幕截图以供肉眼检查。虽然这种方法对于小应用来说可能已经足够了,但是它的缺陷与任何其他手动测试一样 任何变更后都需要耗费人力来处理。另一方面,由于缺乏清晰的定义,自动检测 UI 问题非常具有挑战性——这就是“视觉回归”领域解决这个难题的切入点:对比旧 UI 与最新的更改并检测差异。一些开源/免费的工具可以提供这个能力 (例如: wraith、PhantomCSS) 但可能安装耗时比较久。一些商业工具 (如 Applitools、Percy.io) 则更进一步,它们简化了安装过程,并封装了高级特性,如管理 UI、告警、通过去除“视觉噪音”(如广告、动画) 进行智能捕获,甚至可以分析引发问题的 DOM/css 变化的根本原因。

后端测试

1.丰富你的测试组合:不局限于单元测试和测试金字塔

测试金字塔,虽然已经有超过 10 年的历史了,但是它仍是一个很好的相关模型,它提出了三种测试类型,并且影响了大多数开发人员的测试策略。与此同时,大量闪亮的新测试技术出现了,并隐藏在测试金字塔的阴影下。考虑到近 10 年来我们所看到的所有巨变(微服务、云、无服务器),这个非常老的模型是否仍能适用于所有类型的应用?测试界不应该考虑欢迎新的测试技术吗?

请不要误解,在 2019 年,测试金字塔、TDD、单测仍然是强大的技术,且对于大多数应用仍是最佳选择。但是像其他模型一样,尽管它有用,但是一定会在某些时候出问题。例如,我们有一个 IOT 应用,将许多事件注入一个 Kafka/RabbitMQ 这样的消息总线中,然后这些事件流入一些数据仓库并被分析 UI 查询。我们真的需要花费 50% 的测试预算去为这个几乎没有逻辑的集成中心化的应用写单测吗?随着应用类型(机器人、密码、Alexa-skills)的多样性增长,测试金字塔可能将不再是某些场景的最佳选择了。

是时候丰富你的测试组合并了解更多的测试类型了(下一节会给你一些小建议),这些类似于测试金字塔的思维模型与你所面临的现实问题更匹配('嘿,我们的API 挂了,试试消费者驱动的合同测试!'),让您的测试多样化,比如建立基于风险分析的检查模型 —— 评估可能出现问题的位置,并提供一些预防措施以减轻这些潜在风险。

需要注意的是:软件世界中的 TDD 模型面临两个极端的态度,一些人鼓吹到处使用它,另一些人则认为它是魔鬼。 每个说绝对的人都是错的 :]

否则你将错过一些超高投入产出比的工具,比如 Fuzz、lint、mutation 这些工具只需 10 分钟配置就能贡献价值。

不要写全局的 fixtures 和 seeds,而是放在每个测试中

参照黄金法则,每条测试需要在它自己的 DB 行中运行避免互相污染。现实中,这条规则经常被打破:为了性能提升而在执行测试前全局初始化数据库(也被称为‘test fixture’)。尽管性能很重要,但是它可以通过后面讲的「分组件测试」缓和。为了减轻复杂度,我们可以在每个测试中只初始化自己需要的数据。除非性能问题真的非常显著,那么可以做一定的妥协——仅在全局放不会改变的数据(比如 query)。

否则一部分测试挂了,我们的团队花费大量宝贵时间后发现,是由于两个测试同时改变了同一个 seed 数据导致的。

ava

轻量级的测试框架

https://github.com/avajs/ava

Mocha

安装

npm install mocha --save-dev

使用

const add = require("./add");
const assert = require("assert");

// describe:定义一组测试
describe("加法函数测试", function() {
    before(function() {
        // runs before all tests in this block
    });
    
    // it: 定义一个测试用例
    it("1 加 1 应该等于 2", function() {
        // assert: nodejs内置断言模块
        assert.equal(add(1, 1), 2);
    });
    
    after(function() {
        // runs after all test in this block
    });
});

断言库

Mocha 支持should.js, chai, expect.js, better-assert, unexpected等断言库

//assert
assert.ok(add(1, 1));
assert.equal(add(1, 1), 2);
//shouldjs
(add(1, 1)).should.be.a.Number();
(add(1, 1)).should.equal(2);
//expectjs
expect(add(1, 1)).to.be.a("number");
expect(add(1, 1)).to.equal(2);
//chai支持should, expect, assert三种语法

should.js和expect.js相较于assert语义性更强,且支持类型检测,而should.js在语法上更加简明,同时支持链式语法.and。

  • chai.js断言库:接口丰富,文档齐全,可以对各种接口进行断言。
  • expect 库应用是非常广泛,拥有很好的链式结构和仿自然语言的方法。
  • 通常写同一个断言会有几个方法,比如:expect(response).to.be(true) 和 expect(response).equal(true)。

expect和should是BDD风格的,二者使用相同的链式语言来组织断言,但不同在于他们初始化断言的方式:expect使用构造函数来创建断言对象实例,而should通过为Object.prototype新增方法来实现断言(所以should不支持IE);expect直接指向chai.expect,而should则是chai.should()。

expect断言风格

  • ok :检查是否为真
  • true:检查对象是否为真
  • to.be、to:作为连接两个方法的链式方法
  • not:链接一个否定的断言,如 expect(false).not.to.be(true)
  • a/an:检查类型(也适用于数组类型)
  • include/contain:检查数组或字符串是否包含某个元素
  • below/above:检查是否大于或者小于某个限定值

assert风格是三种断言风格中唯一不支持链式调用的,Chai提供的assert风格的断言和node.js包含的assert模块非常相似。

Mocha 支持4种 hook,包括before / after / beforeEach / afterEach。

Mocha 默认每个测试用例执行2000ms,超出时长则报错,所以在测试代码中如果有异步操作,则需要通过done函数来明确测试用例结束。done接受Error参数。

Mocha 在node环境下运行时,不支持 BOM 和 DOM 接口,需要引入jsdom和jsdom-global库。

Mocha命令行基本用法:

  • mocha:默认运行test子目录里面的测试脚本,不包括子文件
  • mocha add.test.js:当前目录下面的该测试脚本。
  • mocha file1 file2 file3 : mocha命令后面紧跟测试脚本的路径和文件名,可以指定多个测试用例。

通配符:

  • mocha spec/{my,awesome}.js
  • mocha test/unit/*.js

生成格式

  • mocha –reporter spec:默认为spec格式,可设置其他格式。
  • mocha –recursive -R markdown > spec.md 。

网页查看

npm install –save-dev mochawesome

在gulp中运行mocha

安装gulp-mocha插件

npm install gulp-mocha --save-dev

gulpfile

gulp.task('mocha',function() {
	return 
})

Jasmine

Jasmine 是一个功能全面的测试框架,内置断言expect;但是有全局声明,且需要配置,相对来说使用更复杂、不够灵活。

npm install jasmine --save-dev

Jasmine 的语法与 Mocha 非常相似,不过断言采用内置的expect()。

Jest

Jest 是一个功能全面的“零配置”测试框架,既集成了各种工具,且无需配置即可使用。

npm install --save-dev jest

Jest 中以test定义一个测试用例,且自带断言expect,断言库功能强大,但语法相较于should.js来说更复杂。

普通匹配:toBe, not.toBe

空匹配:toBeNull, toBeUndefined, toBeDefine, toBeTruthy, toBeFalsy

数字大小:toBeGreaterThan, toBeGreaterThanOrEqual, toBeLessThan, toEqual, toBeCloseTo(用于浮点数)

正则匹配:toMatch

数组查询:toContain

构造匹配:toEqual(expect.any(constructor))

Jest 同样有四个hook,beforeAll/beforeEach/afterAll/afterEach

Jest 内置对 DOM 和 BOM 接口的支持。

Jest 内置覆盖统计,为了更方便地进行相关配置,我们可以创建一个配置文件jest.config.js

然后将package.json中的命名修改一下:"test-jest": "jest"

jest教程:http://github.yanhaixiang.com/jest-tutorial/#%E6%B5%8B%E8%AF%95%E9%9A%BE%E7%82%B9

写测试的难点在于:

不会配置。 Jest 的上手文档非常简单,甚至不需要配置。但真实情况是只要一个配置没配好,所有测试都跑不起来。测试不像开发,代码有问题可以慢慢调。 测试是一个 0 - 1 游戏,不是成功就是失败,挫败感非常强。

不知道要怎么 Mock。 这个绝对是经典中的经典。虽然官方文档有教程,但是真实的业务往往不是那么理想,远比文档要复杂的多。

不会构造测试用例。 刚接触测试时,很容易把做业务那套 “实现 XXX 功能” 的想法代入测试。但测试的重点不在于实现功能,而是构造用例。

没有测试策略。 上面是 “技” 的难点,测试还有 “术” 的难点。闷着头一通肝测试代码并不高效,使用合适的测试策略远比写 10 个测试用例重要。

好的测试会让你获得很高的代码信心,而不好的测试则会严重拖垮项目开发。所以,大家所厌恶的不应该是测试本身,而是那些维护性差的测试。

测试环境

在很多时候,我们前端的代码往往只在浏览器里运行,经常要用到浏览器的 API。由于 Jest 的测试文件也是 Node.js 环境下执行的,jest 提供了 testEnvironment 配置,添加 jsdom 测试环境后,全局会自动拥有完整的浏览器标准 API。原理是使用了 jsdom (opens new window)。 这个库用 JS 实现了一套 Node.js 环境下的 Web 标准 API。

module.exports = {
  testEnvironment: "jsdom",
}

mock

jest 里有两种 mock,一种是方法的 mock,还有一种是模块的 mock

如果要mock nodejs里的fs模块,就如下写出

const fs = require('fs');
 
jest.mock('fs');
 
console.log(fs.statSync('/tmp/file'));

jest 在执行这个文件的时候,首先会对代码进行转换,转换分成两步。

第一步是提升 jest.mock('fs'),让它能作用在 require 之前,转换后的代码如下

jest.mock('fs');
 
const fs = require('fs');
 
jest.mock('fs');
 
console.log(fs.statSync('/tmp/file'));

第二部是包一层匿名方法,这一步跟 node 的模块实现类似

(function(module, exports, require, __dirname, __filename, global, jest){
  jest.mock('fs');
 
  const fs = require('fs');
 
  console.log(fs.statSync('/tmp/file'));
}))

代码转换完后,jest 需要注入自己的 require 实现,这个一步通过让转换后的代码在 vm 模块创建的新的上下文里执行,最终生成一个可以执行的匿名方法实现。

const vm = require('vm');
 
const code = '转换后的代码';
 
const script = new vm.Script(code);
 
const result = script.runInContext(context); // describe, it 等全局方法在这里注入
 
result.call(
  module,
  exports,
  require, // jest 自己的 require 实现,
  ...
);

Match

代码中的 undefined, null, and false 有不同含义,若你在测试时不想区分他们,可以用真值判断。 Jest 提供 helpers 供你使用

// null、undefined、真值、假值
test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

test('zero', () => {
  const z = 0;
  expect(z).not.toBeNull();
  expect(z).toBeDefined();
  expect(z).not.toBeUndefined();
  expect(z).not.toBeTruthy();
  expect(z).toBeFalsy();
});

// 数值
test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

// 字符串
test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

// 数组
const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'milk',
];

test('shoppingList数组中包含milk', () => {
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});

支持ts

ts-jest

安装jest的ts转译器

npm i -D ts-jest@27.1.4

在 jest.config.js 里添加一行配置

module.exports = {
  preset: 'ts-jest',
  // ...
};

在安装jest的类型文件

npm i -D @types/jest@27.4.1

然后在 tsconfig.json 里加上 jest 和 node 类型声明:

{
  "compilerOptions": {
    "types": ["node", "jest"]
  }
}

也可以选择使用 babel-jest 来做转译,不过Babel 做转译的 缺点是无法让 Jest 在运行时做类型检查,所以更推荐大家使用 ts-jest,利用 tsc 来转译 TypeScript。

React

测试react需要引入testing-library/react库

npm i -D @testing-library/react@12.1.4

也可以使用另一个库 react-test-renderer

npm i -D react-test-renderer

fireEvent

fireEvent 是 React Testing Library 提供的一个函数,用于模拟用户在 UI 上触发特定的事件。而 input 则是其中的一种事件类型,代表在输入框中输入文本。

如果您想在测试中模拟用户在输入框中输入文本,可以使用 fireEvent.input 函数。该函数接受两个参数:要模拟事件的元素(通常是输入框)和一个对象,包含要输入的文本内容

import { render, fireEvent } from '@testing-library/react';

test('should update input value', () => {
  const { getByLabelText } = render(<input aria-label="input" />);
  const input = getByLabelText('input');

  fireEvent.input(input, { target: { value: 'hello world' } });

  expect(input.value).toBe('hello world');
});

https://juejin.cn/post/6907052045262389255?from=search-suggest#heading-15

快照测试

https://juejin.cn/post/7122779014984368164

组件是有 HTML 结构的。 如果不对比一下 HTML 结构,很难说服自己组件没问题。但是这就引来了一个问题了:要怎么对比 HTML 结构?

最简单的方法就是把这个组件的 HTML 打印出来,拷贝到一个 xxx.txt 文件里,然后在下次跑用例时,把当前组件的 HTML 字符串和 xxx.txt 文件里的内容对比一下就知道哪里有被修改过。 这就是快照测试的基本理念,即:先保存一份副本文件,下次测试时把当前输出和上次副本文件对比就知道此次重构是否破坏了某些东西。

只不过 jest 的快照测试提供了更高级的功能:

  1. 自动创建把输出内容写到 .snap 快照文件,下次测试时可以自动对比
  2. 输出格式化的快照文件,阅读友好,开发者更容易看懂
  3. 当在做 diff 对比时,jest 能高亮差异点,而且对比信息更容易阅读

快照测试通过说明渲染组件没有变,如果不通过则有两种可能:

  1. 代码有 Bug。 本来好好的,被你这么一改,改出了问题
  2. 实现了新功能。 新功能可能会改变原有的 DOM 结构,所以你要用 jest --updateSnapshot 来更新快照

在title.test.tsx中添加一个快照测试

// tests/components/Title.test.tsx
import React from "react";
import { render } from "@testing-library/react";
import Title from "components/Title";

describe("Title", () => {
  it("可以正确渲染大字", () => {
    const { baseElement } = render(<Title type="large" title="大字" />);
    expect(baseElement).toMatchSnapshot();
  });

  it("可以正确渲染小字", () => {
    const { baseElement } = render(<Title type="small" title="小字" />);
    expect(baseElement).toMatchSnapshot();
  });
});

执行测试后,会发现在 tests/components/ 下多了一个 Title.test.tsx.snap 文件

// tests/components/Title.test.tsx.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Title 可以正确渲染大字 1`] = `
<body>
  <div>
    <p
      style="font-size: 2em; color: red;"
    >
      大字
    </p>
  </div>
</body>
`;

exports[`Title 可以正确渲染小字 1`] = `
<body>
  <div>
    <p
      style="font-size: 0.5em; color: green;"
    >
      小字
    </p>
  </div>
</body>
`;

快照测试的注意事项

避免大快照

现在 Title 比较简单,所以看起来还可以,但真实业务组件中动辄就有十几个标签,还带上很多乱七八糟的属性,生成的快照文件会变得无比巨大。

对于这个问题,我们能做的就是避免大快照,不要无脑地记录整个组件的快照,特别是有别的 UI 组件参与其中的时候

所以,对于那种输出很复杂,而且不方便用 expect 做断言时,快照测试才算是一个好方法。 这也是为什么组件 DOM 结构适合做快照,因为 DOM 结构有大量的大于、小于、引号这些字符。如果都用 expect 来断言,expect 的结果会写得非常痛苦。 不过,需要注意的是:不要把无关的 DOM 也记录到快照里,这无法让人看懂。

假错误

假如现在把 title 的 “大字” 改成 “我是一个大帅哥,马上就得到一个渲染报错

这里只是文案改了一下,业务代码并没有任何问题,测试却出错了,这就是测试中的 “假错误”。 虽然普通的单测、集成测试里也可能出现 “假错误”, 但是快照测试出现 “假错误” 的概率会更高,这也很多人不信任快照测试的主要原因。

在一些大快照,复杂组件的情况下,只要别的开发者改了某个地方,很容易导致一大片快照报错,基于人性的弱点,他们是没耐心看测试失败的原因的, 再加上更新快照的成本很低,只要加个 --updateSnapshot 就可以了,所以人们在面对快照测试不通过时,往往选择更新快照而不去思考 DOM 结构是否真的变了

这些因素造成的最终结果就是:不再信任快照测试。 所以,你也会发现市面上很多前端测试的总结以及文章都很少做 快照测试。很大原因是快照测试本身比较脆弱, 而且容易造成 “假错误”

总结出快照测试的适用场景:

  • 组件 DOM 结构的对比
  • 在线上跑了很久的老项目
  • 大块数据结果的对比

更新快照

jest --updateSnapshot

运行上面的命令行,并接受更改。 你可以用单个字符完成同样的事情 ,如 jest -u。 这将为所有失败的快照测试重新生成快照文件。 如果我们无意间产生了Bug导致快照测试失败,应该先修复这些Bug,再生成快照文件,以避免用快照录制了错误的行为。

内联快照

内联快照和普通快照(.snap 文件)表现一致,只是会将快照值自动写会源代码中。 这意味着你可以从自动生成的快照中受益,并且不用切换到额外生成的快照文件中保证值的正确性。

组件测试

编写组件测试文件

// tests/components/AuthButton/simple.test.tsx
import { render, screen } from "@testing-library/react";
import AuthButton from "components/AuthButton";
import React from "react";

describe('AuthButton', () => {
  it('可以正常展示', () => {
    render(<AuthButton>登录</AuthButton>)

    expect(screen.getByText('登录')).toBeDefined();
  });
})

直接这样写jest会报错

Jest 不会转译任何内容,因此我们一直用 tsc 来转译 TypeScript。由于 tsc 看不懂引入的 .less,导致了 Unexpected Token 报错

比较推荐的方法是把 .less 转译成空文件

除了 .less 文件,我们还要对非 JS 静态资源做转译,比如 jpg, svg, png 等等(这些不会影响测试)。

npm i -D jest-transform-stub@2.0.0

添加转译配置

// jest.config.js
module.exports = {
  // ...
  transform: {
    ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
  } 
}

redux

react hooks

测试hooks时不能像纯函数一样直接在测试文件中使用hooks,因为react只允许在组件顶层使用hooks

引入@testing-library/react-hooks

npm i -D @testing-library/react-hooks@8.0.0

使用

// tests/hooks/useCounter/renderHook.test.ts
import { renderHook } from "@testing-library/react-hooks";
import useCounter from "hooks/useCounter";
import { act } from "@testing-library/react";

describe("useCounter", () => {
  it("可以做加法", () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current[1].inc(1);
    });

    expect(result.current[0]).toEqual(1);
  });

  it("可以做减法", () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current[1].dec(1);
    });

    expect(result.current[0]).toEqual(-1);
  });

  it("可以设置值", () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current[1].inc(10);
    });

    expect(result.current[0]).toEqual(10);
  });

  it("可以重置值", () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current[1].inc(1);
      result.current[1].reset();
    });

    expect(result.current[0]).toEqual(0);
  });

  it("可以使用最大值", () => {
    const { result } = renderHook(() => useCounter(100, { max: 10 }));

    expect(result.current[0]).toEqual(10);
  });

  it("可以使用最小值", () => {
    const { result } = renderHook(() => useCounter(0, { min: 10 }));

    expect(result.current[0]).toEqual(10);
  });
});

测试异步代码

在JavaScript中执行异步代码是很常见的。 当你有以异步方式运行的代码时,Jest 需要知道当前它测试的代码是否已完成,然后它可以转移到另一个测试。 Jest有若干方法处理这种情况。

为你的测试返回一个Promise,则Jest会等待Promise的resove状态 如果 Promise 的状态变为 rejected, 测试将会失败。

例如,有一个名为fetchData的Promise, 假设它会返回内容为'peanut butter'的字符串 我们可以使用下面的测试代码︰

test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});

或者,您可以在测试中使用 async 和 await。 写异步测试用例时,可以在传递给test的函数前面加上async。 例如,可以用来测试相同的 fetchData 方案

test('the data is peanut butter', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch('error');
  }
});

jest周边工具

Jest-changed-files

用于识别 git/hg 存储库中已修改文件的工具。 导出两个函数:

  • getChangedFilesForRoots 返回一个resolved状态的Promise,此Promise包含修改文件和仓库信息。
  • findRepos 返回一个resolved状态的Promise,此Promise包含指定路径的一组仓库数据。
const {getChangedFilesForRoots} = require('jest-changed-files');

// 打印出当前目录最后修改过的一组文件
getChangedFilesForRoots(['./'], {
  lastCommit: true,
}).then(result => console.log(result.changedFiles));

jest-diff

数据更改的可视化工具。 输出一个函数,两个任何类型的值输入该函数后,返回一个“较易读”的字符串来展现其区别。

const {diff} = require('jest-diff');

const a = {a: {b: {c: 5}}};
const b = {a: {b: {c: 6}}};

const result = diff(a, b);

// 打印两个值的差异
console.log(result);

jest-docblock

提取和解析 JavaScript 文件顶部注释的工具, 导出各种函数来操作注释块内的数据。

const {parseWithComments} = require('jest-docblock');

const code = `
/**
 * This is a sample
 *
 * @flow
 */

 console.log('Hello World!');
`;

const parsed = parseWithComments(code);

// 打印一个包含两个属性的对象: comments and pragmas.
console.log(parsed);

Jest-get-type

用于识别任何 JavaScript 值的原始类型的模块, 模块导出了一个可以识别传入参数类型并将类型以字符串作为返回值的函数。

const {getType} = require('jest-get-type');

const array = [1, 2, 3];
const nullValue = null;
const undefinedValue = undefined;

// prints 'array'
console.log(getType(array));
// prints 'null'
console.log(getType(nullValue));
// prints 'undefined'
console.log(getType(undefinedValue));

Enzyme

这是一个由 Airbnb 编写的包装库,它使得测试 React 组件变得更容易。同时,我们还要为我们使用的 React 不同版本安装适配器

Enzyme的API和jQuery操作DOM一样灵活易用,因为它使用的是cheerio库来解析虚拟DOM,而cheerio的目标则是做服务器端的jQuery。Enzyme兼容大多数断言库和测试框架,如chai、mocha、jasmine等。

安装

npm i --save-dev enzyme enzyme-adapter-react-16

enzyme支持三种方式的渲染:

shallow:浅渲染,是对官方的Shallow Renderer的封装。将组件渲染成虚拟DOM对象,只会渲染第一层,子组件将不会被渲染出来,因而效率非常高。不需要DOM环境, 并可以使用jQuery的方式访问组件的信息;

render:静态渲染,它将React组件渲染成静态的HTML字符串,然后使用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,可以用来分析组件的html结构。

mount:完全渲染,它将组件渲染加载成一个真实的DOM节点,用来测试DOM API的交互和组件的生命周期,用到了jsdom来模拟浏览器环境。

enzyme中有几个比较核心的函数需要注意,如下:

simulate(event, mock):用来模拟事件触发,event为事件名称,mock为一个event object;

instance():返回测试组件的实例;

find(selector):根据选择器查找节点,selector可以是CSS中的选择器,也可以是组件的构造函数,以及组件的display name等;

at(index):返回一个渲染过的对象;

get(index):返回一个react node,要测试它,需要重新渲染;

contains(nodeOrNodes):当前对象是否包含参数重点 node,参数类型为react对象或对象数组;

text():返回当前组件的文本内容;

html(): 返回当前组件的HTML代码形式;

props():返回根组件的所有属性;

prop(key):返回根组件的指定属性;

state():返回根组件的状态;

setState(nextState):设置根组件的状态;

setProps(nextProps):设置根组件的属性;

利用enzyme渲染、操作dom,这样就可以配合Jest进行测试

import React from 'react'
import { mount } from 'enzyme'
import TodoItem from '../../src/components/TodoItem.jsx'

describe('待办事项-列表项组件', () => {
  test('渲染待办事项列表项', () => {
    const todo = {id: 2, title: '复习 React Hooks 使用', completed: false}
    const wrapper = mount(
      <TodoItem todo={todo} />
    )
    const p = wrapper.find('p')
    expect(p.text()).toBe('复习 React Hooks 使用')
  })
})

Sinon

Vitest

视觉测试

resemblejs

如果你觉得我的文章对你有帮助的话,希望可以推荐和交流一下。欢迎關注和 Star 本博客或者关注我的 Github。