提升代码质量:不可忽视的 JavaScript 单元测试技巧
从基础的测试框架到高级的测试策略,本篇文章将带你深入探索JavaScript测试开发的方方面面,无论你是刚接触测试的初学者还是想进一步提升代码可靠性的资深开发者,都能在这里找到实用的技巧与实践经验,让我们一起开启这段“无 bug 代码”的探索之旅吧!
你可能会想:“测试真的那么重要吗?它能带来什么实质性的改变?” 如果你也是一个注重质量和效率的开发者,那么接下来的内容会让你对 JavaScript 测试有全新的认识。
目录
单元测试
什么是单元测试:软件测试中的一种方法,它将程序的最小可测试单元(通常是一个函数或方法)进行独立测试以确保其功能按预期工作,通过单元测试开发者可以验证每个组件在孤立环境中的行为是否符合要求,从而提高代码的可靠性和可维护性。
单元测试的作用:当我们在开发的时候想进行快速迭代,持续交付用户价值,那我可以明确告知如果我们不写单元测试我们的开发就快不起来,因为每次开发和每次发布团队都要投入人力来进行手工测试,其中也包括自己,而且因为没有测试自己往往倾向于不敢随意重构代码,而这又导致代码逐渐腐化项目风险逐渐升高,随着项目的进展代码越来越复杂而这样的复杂程度会使开发速度再次降低,这样就陷入了一个死循环,代码越来越烂自己写起来就越慢,也会导致出现更多的bug:

在我们写JavaScript代码时,追求的是高效灵活和可维护性,但无论多么精心设计的功能也可能在某个小小的更新后引发意外的bug,这时单元测试就成了我们维护代码质量的重要工具,它不仅能帮助我们确保功能按预期运行还能在代码更新时提前发现潜在的错误,并且我们也可以在项目中搭建流水线持续集成我们的产品代码和测试代码,持续不断验证每一次提交代码是否符合预期:

不做测试的前提:项目中要不要做单元测试取决于实际场景,如果我们的业务不需要频繁上线并且整个团队有足够多的人力来覆盖手工测试,那我们就不需要用单元测试,或者如果说我们不在意这个代码是否变得很烂,并且也从来不做重构也不想让自己的代码变得更好,那我们也不需要用单元测试,说白了进行单元测试会无形增加开发时间但缩小测试时间:

有两个团队在进行同样的项目,一个团队进行单元测试,另外一个团队不进行单元测试,让我们来想一下他们整体项目的进展情况,分析如下,虽然单元测试增加了实施功能所需的时间,但是单元测试提升了代码质量的可维护性,使得产品的交付周期缩短:

有人可能会遇到平常开发过程中进行单元测试很少发现问题,这不是浪费精力吗?这一点就很好的说明了保障性的含义,你开发的代码不可能永远都是你维护,假设有一个现有的功能需要交给别人去升级迭代,别人怎么保证项目代码之前的功能不受影响,这个时候单元测试就体现出作用了,别人只需要完整的运行项目中原本的测试用例,就可以保证之前的功能不会被break掉!
在JavaScript测试框架中常见的有几种,适用于不同的场景,以下是一些流行的JS测试框架:
1)Jest(推荐):广泛用于React和Node.js应用程序的单元测试和集成测试,零配置,支持模拟快照测试,异步测试等,广泛用于前端开发尤其适合React项目
2)Ava:一个简单且快速的测试框架,支持并行测试和异步测试,简洁的语法自动清理测试环境,适用于快速开发和执行测试
3)Jasmine:最早的行为驱动开发(BDD)测试框架之一,支持前端和后端JavaScript测试,没有依赖项、内建断言和模拟功能,语法简洁易于上手,适合行为驱动开发
4)Mocha:一个灵活的测试框架,通常与其他库(如 Chai、Sinon)结合使用,提供丰富的功能比如测试生命周期钩子、异步支持、断言库等,适用于Node.js和浏览器端的测试
Jest介绍与使用
Jest:是Facebook开发的一款JS测试框架,在Facebook内部广泛用来测试各种JS代码,它具备轻松上手;高性能支持测试的并发运行;内置强大的断言与mock功能;内置测试覆盖率统计功能;内置Snapshot功能等,官方文档:地址 ,其特点如下所示:

为了方便后期项目的使用,我们可以全局安装Jest,执行 npm i jest -g 即可,接下来我们开始讲解Jest的具体使用,终端执行如下命令进行项目安装:
npm i jest -D
安装完成之后我们在package.json文件中添加运行测试用例的命令:
"scripts": {
"test": "jest"
},
在开始使用Jest之前,我们要先知道Jest支持的三种方式来写测试代码,如下所示:

Jest最基础的测试结构有三部分组成,describe、test、expect,如下所示:

匹配器使用:然后我们创建一个index.js文件写下add函数之后,当我们想测试代码的时候就可以新建一个test的js文件,node会自动识别当前文件为测试用例代码,然后测试用例代码写下测试的代码和结果,运行test命令之后就可以校验当前我们的代码的准确性,当我们接收的结果不等于执行后的结果时,Jest就会报错,如下所示:

当然如果设置的是TS文件的话,进行类型校验之后test的函数一般没有引入依赖就会报错,这里我们可以借助使用Jest附带的类型定义,每次更新Jest时都会更新安装@jest/global als包,然后从中导入API:
pnpm add --save-dev @jest/globals
import {describe, expect, test} from '@jest/globals';
import {sum} from './sum';
describe('sum module', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
});
在上面的代码中,expect (1 + 2) 返回了一个"预期"的对象,通常不会对这些期望对象调用过多的匹配器。 在此代码中.toBe(3)是匹配器,当Jest运行时它会跟踪所有失败的匹配器以便它可以为你打印出很好的错误消息,对于匹配器的使用可以参考官方文档:地址,常用的匹配器如下所示:

当然还有一些复杂的情况,比如说一个函数本身并没有返回值,那么我们如何测试其代码能够被真正执行呢? 我们可以通过assertions进行预期断言,然后定义一个函数并在函数内部执行打印,然后测试函数是否会打印我们的内容,来判断当前函数的执行情况:

模拟函数使用:Mock函数允许测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用new实例化时捕获构造函数的实例、允许测试时配置返回值,如下示例我们通过describe进行测试用例分组,将对map函数的测试代码放在一起进行测试验证,可以验证当前代码的返回值和执行次数:

代码覆盖率:后期项目的测试用例多了的话,我们可以在命令后面添加一个--coverage命令,会自动帮助我们生成覆盖率情况,

运行生成文件夹下的html文件,也会帮助我们打开一个浏览器界面进行可视化展示测试数据:

Jest框架mock的方式有很多,如下所示:
1)Jest.fn() 创建一个mock方法
2)Jest.spyOn() 创建类似于jest.fn的模拟函数,但也跟踪对对象[methodName]的调用
3)Jest.mock(moduleName, factory, options)
为了Jest测试用例代码的简便,我们也可以将公共用到的方法或变量进行预处理和后处理操作:
beforeAll/afterAll:对测试文件中所有的用例开始前/后进行统一的预处理
beforeEach/afterEach:在每个用例开始前/后进行都预处理
快照测试:当我们进行代码测试用例完成之后,后期如果想知道我们是否修改了关键代码可以添加一个测试用例的快照,典型的做法是在渲染了UI组件之后保存一个快照文件, 检测他是否与保存在单元测试旁的快照文件相匹配,若两个快照不匹配测试将失败:有可能做了意外的更改或者UI组件已经更新到了新版本,如果开发者同意修改,则执行如下命令更新快照:
jest --updateSnapshot

异步测试:在测试代码中如果我们想通过异步方法进行测试,可以采用以下几种方式:
// it的方法是test方法的别名,Callback回调函数测试
it('the data is apple', done => {
function callback(data) {
expect(data).toBe('apple')
done() // 异步方法结束
}
callback('apple')
})
// Promise方式测试
it('the data is apple', () => {
expect.assertions(1)
return fetchData().then(data => {
expect(data).toEqual('apple')
})
expect(Promise.resolve('apple')).resolves.toBe('apple')
expect(Promise.reject(new Error('octopus'))).resolves.toBe('octopus')
})
// async/await方法测试(推荐写法)
test('the data is apple', async () => {
// given
const params = {}
// when
const result = await fetchData(params)
// then
expect(result).toBe('apple')
})
Vitest介绍与使用
Vitest:一个基于Vite构建的现代化JavaScript测试框架,主要用于单元测试、集成测试和端到端测试,它旨在提供快速高效的测试体验,特别适合与Vite项目配合使用但也可以用于其他环境,其官方文档:地址 如下所示,vitest是兼容Jest的,所以说Vitest的api与Jest非常相似,用户从Jest迁移到Vitest基本上没啥学习成本:

接下来我们终端执行如下命令安装测试插件:
npm install -D vitest
Vitest特别强大的地方在于它能实时监听测试的更改,像官方文档所言就像是给测试添加了热更新一样,如下我们配置好test之后,写一个demo演示了一下,当修改测试内容,控制台会实时打印测试后的结果,非常的丝滑,牛逼!!!


Vitest对于新手来说特别的友好,基本上不用自己去配置复杂的配置文件,上手就能拿来使用,就像我们直接导入一个vue文件并解析打印这个组件,可以看到测试代码也是通过的:

全局导包:在vitest中使用测试代码的api还是要进行引入依赖的,不然ts类型就会校验错误,但是我们可以通过一个全局导包的方式来实现自动引入依赖而不需要再手动进行引入,如下我们现在vite.config.ts文件中进行一个test配置,然后在tsconfig文件进行一个设置:

当然如果想把vite和vitest的配置区分开来的话,我们可以新建一个vitestConfig文件,里面写好相关的vitest配置之后再引入到vite配置当中,如下所示:

组件测试:如果我们想进行组件测试的话,可以安装如下插件,然后配置好当前环境js能够解析dom元素:
npm i @vue/test-utils jsdom -D

然后我们可以获取当前组件的text文件内容并且进行一个测试对比:

解析jsx文件:当前vite采用的vue框架如何想写tsx文件并进行单元测试的话,可以安装如下插件来实现对jsx等文件的解析:
npm i @vitejs/plugin-vue-jsx -D
安装完插件之后我们就需要在vite配置文件中进行引入,然后在vitest配置文件中我们需要指定当前的tsx文件的运行环境是web,因为vitest的tsx文件默认环境是ssr,如果我们不指定web的话默认就会报错,配置如下所示:

配置完成之后我们引入tsx文件然后进行单元测试,可以看到效果依旧生效:

当然涉及到真正复杂的业务的时候我们编写的测试用例的代码就需要根据具体情况使用,示例如下
import { describe, it, expect, beforeEach } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useTodoStore } from "./todo"
describe("todoStore", () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it("添加一个item之后,todos里面应该包含这个item", () => {
const store = useTodoStore()
store.addTod('吃饭')
expect(store.todos).toContainEqual({ id: 1, text: '吃饭' })
})
it("删除一个item之后,todos里面不应该包含这个item", () => {
const store = useTodoStore()
store.addTod('吃饭')
store.removeTod(0)
expect(store.todos).not.toContainEqual({ id: 1, text: '吃饭' })
})
it("更新一个item之后,todos里面应该包含这个item", () => {
const store = useTodoStore()
store.addTod('吃饭')
store.updateTod(0, '睡觉')
expect(store.todos).toContainEqual({ id: 1, text: '睡觉' })
})
})
组件UI测试
上面讲解了Jest的一些基本概念及其使用案例,接下来我们开始学习如何对组件UI进行测试,我们以Testing Library为例来测试React组件,当然Testing Library也同时适用于build或其他框架,坦白来讲,对UI界面级别的测试是非常困难的一件事情,测试页面视图是否显示正常及功能正确并且性能符合预期,多数情况下属于人工测试。
Testing Library:一组用于测试前端组件的工具,它的核心理念是尽可能地模拟用户的行为进行测试而不是关注实现细节,它提供了简单且强大的API来让你测试组件如何与用户交互,同时其也适用于各大前端框架,能够更有效地测试Ul组件也是一种测试方法的最佳实践,其官方文档如下所示:地址 :

以下是使用Testing Library的一个简单示例:
import { screen, render } from '@testing-library/react'
test('renders learn react link', () => {
// given
render(<App />)
// when
const linkElement = screen.getByText(/learn react/i)
// then
expect(linkElement).toBeInTheDocument()
})
其常用的查询组件渲染元素的方法如下所示:
| type | No Match | 1 Match | 1+Match | Await? |
|---|---|---|---|---|
| getBy | throw | return | throw | No |
| findBy | throw | return | throw | Yes |
| queryBy | null | return | throw | No |
| getAllBy | throw | array | array | No |
| findAllBy | throw | array | array | Yes |
| queryAllBy | [] | array | array | No |
当我们涉及到一些交互行为的测试的时候,以下是常用的监听事件:

Cypress组件测试(端到端)
在对前端组件进行UI测试的时候,有一个非常大的痛点也就是我们只能在终端里面去看我们的组件是否表现正常,而且还有一个点在于我们只能把单元测试跑在JSDOM上面,也就是没有真实的去跑在浏览器上所以可能会有某些行为表现不一致
Cypress:是一个强大的端到端(E2E)测试框架,专门用于测试现代Web应用程序的前端部分,它提供了一种快速可靠且易于使用的方式来进行自动化测试,特别适用于开发者和测试人员在开发过程中验证和调试前端应用,所以如果我们考虑到单元测试更高一级的组件集成测试的话,我觉得在Cypress里面是一个非常好的一个尝试,其官网:地址 如下所示:

终端执行如下命令进行安装Cypress:
pnpm add cypress @cypress/react -D
后面阅读官方文档进行尝试吧,这里不再演示了:

更多推荐
所有评论(0)