主页 > 开源代码  > 

Jest单元测试

Jest单元测试

由于格式和图片解析问题,可前往 阅读原文

前端自动化测试在提高代码质量、减少错误、提高团队协作和加速交付流程方面发挥着重要作用。它是现代软件开发中不可或缺的一部分,可以帮助开发团队构建可靠、高质量的应用程序

单元测试(Unit Testing)和端到端测试(End-to-End Testing)是两种常见的测试方法,它们在测试的范围、目的和执行方式上有所不同。单元测试和端到端测试不是相互排斥的,而是互补的。它们在不同的层面和阶段提供了不同的价值,共同构成了一个全面的测试策略

单测和端测区别 单元测试(Unit) 单元测试关注于最小的可测试单元,如函数、方法或模块目的是验证代码中的每个独立单元(如函数)是否按照预期工作通常是自动化的、快速执行的,且不依赖于外部资源或其他模块验证单个代码单元的行为,提供快速反馈,并帮助捕获和修复问题 端到端测试(End-to-End) 从用户角度出发,测试整个应用程序的功能和流程模拟真实的用户交互和场景,从应用程序的外部进行测试。跨多个模块、组件和服务进行,以确保整个应用程序的各个部分正常协同工作涉及用户界面(UI)交互、网络请求、数据库操作等,以验证整个应用程序的功能和可用性

总之,单元测试主要关注代码内部的正确性,而端到端测试关注整体功能和用户体验。结合使用这两种测试方法可以提高软件的质量和可靠性。在项目中尤其是公共依赖如组件库至少都需要单测,端测相对来说比较繁琐点,但是也是程序稳定的重要验证渠道

单元测试 - Jest

这里使用Jest作为单元测试工具,Jest 是一个用于 JavaScript 应用程序的开源测试框架。它是由 Facebook 开发和维护的,通常用于单元测试。Jest 具有简单易用的 API、丰富的功能和强大的断言库,广泛应用于前端开发和 Node.js 环境中

安装 ➜ npm install jest -D 初始化

使用npx进行交互式生成默认的配置文件,它会提示你每步的选择:

➜ npx jest --init The following questions will help Jest to create a suitable configuration for your project ✔ Would you like to use Jest when running "test" script in "package.json"? … yes ✔ Would you like to use Typescript for the configuration file? … no ✔ Choose the test environment that will be used for testing › jsdom (browser-like) ✔ Do you want Jest to add coverage reports? … yes ✔ Which provider should be used to instrument code for coverage? › v8 ✔ Automatically clear mock calls, instances, contexts and results before every test? … yes ✏️ Modified test/package.json 📝 Configuration file created at test/jest.config.js

默认配置文件大概是下面的内容:配置中有很多注释提供我们参考,对于默认的配置就不用删除多语的注释了,方便参考。通常都是对需要的配置项做修改即可

const config = { // All imported modules in your tests should be mocked automatically // automock: false, // Automatically clear mock calls, instances, contexts and results before every test clearMocks: true, // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, // An array of glob patterns indicating a set of files for which coverage information should be collected // collectCoverageFrom: undefined, // The directory where Jest should output its coverage files coverageDirectory: "coverage", // An array of regexp pattern strings used to skip coverage collection // coveragePathIgnorePatterns: [ // "/node_modules/" // ], // Indicates which provider should be used to instrument code for coverage coverageProvider: "v8", // Make calling deprecated APIs throw helpful error messages // errorOnDeprecated: false, // A list of paths to directories that Jest should use to search for files in // roots: [ // "<rootDir>" // ], // The test environment that will be used for testing testEnvironment: "jsdom", // 省略其他... }; module.exports = config;

常用的配置:

collectCoverage:boolean值,用来生成覆盖率报告,通常也可以使用命令行--coverage参数生成

moduleFileExtensions:对于引入文件可以省去文件后缀,jest会根据规则一一匹配

moduleNameMapper:模块匹配规则,告诉jest改模块的匹配路径

{ moduleNameMapper: { // 当匹配到 .css 等结尾的文件时对应 /__mocks__/style-mock.ts 文件 "\\.(css|less|scss|sass)$": "<rootDir>/__mocks__/style-mock.ts", // 当匹配 @ui 开头的的对应到 src 文件夹 "@ui/(.*)": "<rootDir>/src/$1", }, }

setupFiles:在测试环境准备后和安装jest框架前做一些配置,常用来添加一些全局环境模拟数据

setupFilesAfterEnv:在安装jest框架后对jest做一些扩展,相比setupFiles更加通用

testEnvironment:jest模拟的环境,可以选择node、jsdom来模拟node和浏览器环境

testMatch:指定要测试哪些文件

transform:使用一些插件对代码进行转义以便jest可以理解,如设置tsx转义

以上是最基本的配置,jest的配置还是很多的,还要官方有列举了一个表可以随时翻阅不用死记

转译器

Jest中有转义器的概念来帮助它理解编写的代码,可以比做babel对代码做一些转换来兼容浏览器,差不多一样的道理

模块引用转换

在单个测试文件中都会引入我们编写的代码,然后对此代码的功能进行测试,而前端通常都是以esmodule的形式进行函数的导出,jest默认使用的是commonjs,对于module语法jest不认识就会报错

import { sum } from "../core"; // 报错 describe("第一个测试", () => { // ... })

那么可以对jest添加转义器将esmodule模块的代码转换成commonjs就可以了。打开配置文件:

// jest.config.js { transform: { "^.+\\.(ts|tsx|js|jsx)$": [ "babel-jest", { presets: [["@babel/preset-env", { targets: { node: "current" } }]] }, ], }, }

上面使用了 babel-jest和 @babel/preset-env的依赖包需要安装下:

➜ npm i babel-jest @babel/preset-env -D

这样就可以解决esmodule语法不识别的问题

转换typescript:目前项目中的文件都是以ts编写的,而默认情况下jest只识别js文件的,那么就需要对ts进行转译让jest识别

// jest.config.js { transform: { "^.+\\.(ts|tsx|js|jsx)$": [ "babel-jest", { presets: [/* 其它... */["@babel/preset-typescript"]] }, ], }, }

需要安装对应的@babel/preset-typescript;除了使用ts转义器也可以使用ts-jest直接运行ts代码

得益于ts的转译插件可以让jest的测试文件(或配置文件)都写成ts类型的,而在ts文件中对于不识别的jest工具会报错或者没有提示,安装jest的类型文件包@types/jest来告诉ts对应的jest类型,然后进行配置:

// tsconfig.json { "types": ["jest"] }

转换jsx:假如项目中使用了jsx那么也要对jsx进行转义,这里以vue jsx为例

// jest.config.ts { transform: { "^.+\\.(ts|tsx|js|jsx)$": [ "babel-jest", { // 省略其他 plugins: ["@vue/babel-plugin-jsx"], }, ], }, } 基本断言

基本环境配置好后,就到了测试的时间了,我们先来最简单的配置用起

// __tests__/demo.spec.ts import { sum } from "src/utils"; describe("第一个测试", () => { it("分组1", () => { expect(sum(1, 2)).toBe(3); }); }); // 或者不用分组 test("第一个测试", () => { expect(sum(1, 2)).toBe(3); });

这里介绍下几个关键字基本概念:

describe:用来描述当前测试的整体内容it:用来分组测试test:用来描述当前测试,无分组expect:判断参数的值,其的返回值有多个断言方法,上面使用了toBe也就是等于的意思。除了次此断言有很多断言的条件,你可以点击这里阅读官方文档 执行测试 # 现在package中配置 jest 脚本,然后执行测试 ➜ npm run test # npx jest PASS __tests__/demo.spec.ts 第一个测试 ✓ 分组1 (2 ms) ----------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ----------|---------|----------|---------|---------|------------------- All files | 100 | 100 | 100 | 100 | utils.ts | 100 | 100 | 100 | 100 | ----------|---------|----------|---------|---------|------------------- Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.519 s Ran all test suites. ✨ Done in 1.02s.

可以看到对应的测试文件、分组以及测试覆盖率

路径映射

上面在测试代码时会先引入对应的工具代码,如果都使用相对路径引入会显得很麻烦。在项目中通常都喜欢使用@这种方式引入文件,在测试环境依然可以使用,这样也可以和项目中的文件路径保持一致

配置路径映射需要满足两个条件:

jest识别路径映射ts识别路径映射(如果项目中用了ts) 配置jest路径映射 // jest.config.ts { moduleNameMapper: { "@/(.*)": "<rootDir>/src/$1", }, } 配置tsconfig // tsconfig.json { "paths": { "@/*": ["src/*"] } }

这样在测试文件中就可以使用路径映射降低心智负担

// __tests__/demo.spec.ts import { sum } from "@/utils";

除了手动设置外还可以将tsconfig中的path直接作为路径映射,这样就减少了多处的修改。实现这一功能需要借助ts-jest工具包,不同这个自己也可以写个逻辑实现

// jest.config.ts const { pathsToModuleNameMapper } = require('ts-jest/utils') const { compilerOptions } = require('./tsconfig') export default { moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/", }), } dom测试

jest支持浏览器环境,使用浏览器环境时需要安装对应的包,请根据版本可以选择jsdom或jest-environment-jsdom包进行安装,这里jest版本为28+使用后者。测试文件修改如下:

// __tests__/demo.spec.ts describe("第一个测试", () => { it("分组1", () => { // 使用 localStorage API localStorage.setItem('a', '1'); expect(localStorage.getItem(('a'))).toBe('1') }); });

运行测试用例:

➜ npm run test PASS __tests__/demo.spec.ts 第一个测试 ✓ 分组1 (2 ms) ----------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ----------|---------|----------|---------|---------|------------------- All files | 0 | 0 | 0 | 0 | ----------|---------|----------|---------|---------|------------------- Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.701 s, estimated 1 s Ran all test suites. ✨ Done in 1.13s. 异步测试

jest可以使用多种方式进行异步代码测试,通常使用promise、async就可以了

使用promiseasync/await回调

这里模拟一个异步方法,通过上面的三种方式进行测试

// src/utils export function getUser(name: string) { return new Promise((resolve) => { setTimeout(() => resolve(name), 1000); }); } 使用Promise // __tests__/demo.spec.ts import { getUser } from "@/utils"; describe("测试异步代码", () => { it("promise调用方式测试", () => { const user = "小明"; // 使用then getUser(user).then((res) => { expect(res).toBe(user); }); }); it("使用resolves测试promise", () => { const user = "小李"; // 使用 .resolves 方式,注意这里要 return return expect(getUser(user)).resolves.toBe(user); }) }); 使用async测试 // __tests__/demo.spec.ts import { getUser } from "@/utils"; describe("测试异步代码", () => { it("使用async测试", async () => { const user = "小明"; const res = await getUser(user) expect(res).toBe(user); }) }); 使用回调函数

回调函数默认通常是以前那种回调写法,这里需要对以上的异步函数进行调整,让其换成回调函数模式

// 接受一个cb,这里固定返回的值为true,没有错误 export function getUser(cb: (error: any, data: any) => void) { setTimeout(() => { cb(null, true); }, 500); } // 定义测试 describe("测试异步代码", () => { it("使用回调函数", (done) => { function cb(error: any, data: any) { if (error) { done(error); return; } try { expect(data).toBe(true); done(); } catch (err) { done(err); // 这里一定要使用try catch,防止出错时没有执行done } } getUser(cb); }); });

回调模式一定要执行done函数,如果没有执行则会被认为超时错误

模拟函数

假设要模拟一个工具函数的内部实现,可以使用mock函数来判断函数内部的值是否达到预期

定义个待测试的函数forEach

export function forEach(items: number[], callback: (num: number) => void) { for (let index = 0; index < items.length; index++) { callback(items[index]); } }

添加测试用例:

// __tests__/demo.spec.ts import { forEach } from "@/utils"; // 模拟函数 const mockFn = jest.fn((x: number) => x + 1); test("模拟函数", () => { forEach([0, 1], mockFn); expect(mockFn.mock.calls).toHaveLength(2); expect(mockFn.mock.calls[0][0]).toBe(0); expect(mockFn.mock.calls[1][0]).toBe(1); expect(mockFn.mock.results[0].value).toBe(1); });

更多关于模拟函数的例子请查看文档 和 API

定时器

Jest可以通过一个函数转换计时器以便允许你控制时间流量

假设测试如下定时器代码:

export function useTimer(cb?: Function) { setTimeout(() => cb && cb(), 1000); }

编写测试用例:

import { useTimer } from "@/utils"; jest.useFakeTimers(); jest.spyOn(global, "setTimeout"); test("test timer", () => { const cb = jest.fn(); useTimer(cb); expect(cb).not.toBeCalled(); // 执行所有的定时器 jest.runAllTimers(); expect(cb).toBeCalled(); });

更多见官方文档

setup配置

写测试的时候你经常需要在运行测试前做一些准备工作,和在运行测试后进行一些收尾工作。 Jest 提供辅助函数来处理这个问题

这其中包括beforeEach、afterEach、beforeAll、afterAll,其中前两者在每个测试前都会执行一次,后者在文件中只会执行一次

覆盖率

除了对程序进行断言外,jest还收集代码的测试覆盖率并生成对应的报告,包括:某个函数内部的测试覆盖率、整个文件的覆盖率,要想达到覆盖率100%,就要测试到每个文件的所有代码、每个函数内部的所有分支条件

开启覆盖率

可以通过配置文件

// jest.config.ts // 主要涉及到这两个配置 export default { collectCoverage: true, // 启用 coverageDirectory: "coverage", // 报告生成位置 }

通过cli,执行脚本时带上参数

➜ npx jest --coverage 测试覆盖率

假设我们有这么一个函数

export function whatType(arg: any) { const type = Object.prototype.toString.call(arg) if (type === '[object String]') { return 'string'; } else if (type === '[object Boolean]') { return 'boolean'; } }

添加测试用例

import { whatType } from "@/utils"; describe("测试覆盖率", () => { it("函数条件覆盖率", () => { expect(whatType(true)).toBe("boolean"); }); });

执行测试用例

➜ npm run test PASS __tests__/demo.spec.ts 测试覆盖率 ✓ 函数条件覆盖率 (1 ms) ----------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ----------|---------|----------|---------|---------|------------------- All files | 96.77 | 50 | 100 | 96.77 | index.ts | 96.77 | 50 | 100 | 96.77 | 4 ----------|---------|----------|---------|---------|------------------- Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.486 s, estimated 1 s File:测试的文件Stmts:测试中被执行的代码语句的比例Branch:测试代码条件分支比例Funcs:测试中被执行函数比例Lines:测试中被执行代码行数比例Uncovered Line:没有测试到的行数

除了查看终端的表格外,还可以使用更直观的报告,文件报告的结构大概如下:

coverage ├── clover.xml # xml格式 ├── coverage-final.json # json格式 ├── lcov-report # html格式 │ ├── base.css │ ├── block-navigation.js │ ├── favicon.png │ ├── index.html # 主页面入口 │ ├── index.ts.html │ ├── other.ts.html │ ├── prettify.css │ ├── prettify.js │ ├── sort-arrow-sprite.png │ └── sorter.js └── lcov.info

一般都来查看HTML报告,打开报告页面

可以点击对应的文件查看更详细的报告

Vue组件测试

jest也可以对vue组件进行测试,vue官方提供了 vue2版本工具包(vue-test) 和 vue3版本工具包(@vue/test-utils),这里基于vue3组件进行测试

安装对应的依赖:

➜ npm install @vue/test-utils -D

对于Jestv28+以上版本还需要添加以下配置:

// jest.config.ts export default { testEnvironmentOptions: { customExportConditions: ["node", "node-addons"], }, }

创建一个简单的Button组件:

import { defineComponent } from "vue"; export default defineComponent({ render(){ return <button>按钮</button> } })

添加测试用例:

import { mount } from "@vue/test-utils"; import Button from "@/components/Button"; test("测试vue组件", () => { const wrapper = mount({ setup() { return () => { return <Button />; }; }, }); expect(wrapper.text()).toBe('按钮') })

运行测试

➜ npm run test PASS __tests__/demo.spec.tsx ✓ 测试vue组件 (9 ms) ------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ------------|---------|----------|---------|---------|------------------- All files | 100 | 100 | 100 | 100 | Button.tsx | 100 | 100 | 100 | 100 | ------------|---------|----------|---------|---------|------------------- Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.633 s

添加全局组件,当单测某个组件时,组件内部引用的其它组件会因为没有引用而报错,定义全局组件可以解决这个问题

// jest.setup.ts import { config } from "@vue/test-utils"; import Button from "@/button/src/button"; import Icon from "@/button/src/icon"; config.global ponents = { Button, Icon, };

配置jest

// jest.config.ts export default { setupFiles: ["<rootDir>/jest.setup.ts"], }

这里不对vue工具包API过多的解释,更多的API使用可以查看官方文档,vue2版本的可以查看这里

由于格式和图片解析问题,可前往 阅读原文

标签:

Jest单元测试由讯客互联开源代码栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“Jest单元测试