Jest 使用文档
一、引言
1.1 软件测试的重要性
在软件开发的生命周期中,软件测试是确保软件质量、稳定性和可靠性的关键环节。通过有效的测试,可以发现代码中的潜在缺陷、错误和逻辑漏洞,避免在生产环境中出现严重问题,降低维护成本,提高用户满意度。软件测试不仅有助于提升产品质量,还能增强团队的信心,促进项目的顺利推进。
1.2 Jest 的诞生背景与发展历程
Jest 由 Facebook 开发并开源,旨在解决 JavaScript 项目在测试过程中面临的各种挑战。随着 JavaScript 在前端和后端开发中的广泛应用,对高效、易用的测试框架的需求日益增长。Jest 应运而生,它凭借其简洁的语法、强大的功能和良好的性能,迅速在 JavaScript 开发者社区中获得了广泛的认可和使用。自发布以来,Jest 不断更新迭代,添加了许多新特性和功能,逐渐成为 JavaScript 测试领域的主流框架之一。
二、Jest 简介
2.1 定义与功能概述
Jest 是一个 JavaScript 测试框架,它提供了一系列工具和功能,帮助开发者编写和运行测试用例。Jest 的核心功能包括简单易用的断言库、自动模拟机制、快照测试、测试覆盖率报告生成等。通过这些功能,Jest 能够让测试过程变得更加简单、高效,同时提高测试的准确性和可靠性。
2.2 特点与优势
- 简洁易用:Jest 的语法简洁明了,易于上手。开发者可以通过简单的 API 编写测试用例,减少学习成本。
- 自动模拟:Jest 能够自动模拟模块和函数,大大简化了测试过程中对依赖项的处理。
- 快照测试:快照测试功能可以方便地对比代码输出的变化,确保代码的行为符合预期。
- 快速高效:Jest 采用了并行测试和缓存机制,能够显著提高测试的执行速度。
- 丰富的插件生态:Jest 拥有丰富的插件和扩展,开发者可以根据项目需求进行定制化配置。
2.3 与其他测试框架的对比
在 JavaScript 测试领域,除了 Jest,还有 Mocha、Jasmine 等知名测试框架。与 Mocha 相比,Jest 具有更简洁的语法和更强大的自动模拟功能,无需额外配置复杂的断言库和模拟工具。与 Jasmine 相比,Jest 在性能和功能上更具优势,尤其是在处理大型项目和复杂测试场景时表现更为出色。
三、安装与配置
3.1 环境要求
在安装 Jest 之前,确保你的开发环境满足以下要求:
- Node.js:Jest 是基于 Node.js 运行的,因此需要安装 Node.js 环境。建议使用 Node.js 的长期支持版本(LTS)。
- npm 或 yarn:用于安装 Jest 及其依赖包。
3.2 安装方式
- 全局安装:通过 npm 或 yarn 全局安装 Jest,可以在任何项目中使用 Jest 命令。
npm install -g jest
# 或者
yarn global add jest
- 项目内安装:在项目根目录下安装 Jest,将其作为开发依赖项。
npm install --save-dev jest
# 或者
yarn add --dev jest
3.3 配置文件
Jest 的配置文件通常为jest.config.js,位于项目根目录下。以下是一些常见的配置项:
- testMatch:指定测试文件的匹配模式,默认值为["/tests//.js?(x)", "**/?(.)+(spec|test).js?(x)"]。
- coveragePathIgnorePatterns:指定哪些文件不生成测试覆盖率报告,例如["/node_modules/"]。
- moduleNameMapper:用于模块路径映射,例如将@/映射到项目的src/目录。
module.exports = {
testMatch: ["**/*.test.js"],
coveragePathIgnorePatterns: ["/node_modules/"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1"
}
};
3.4 自定义配置
除了上述常见配置项,Jest 还支持许多其他配置选项,如测试环境、测试脚本、模拟函数的配置等。开发者可以根据项目的具体需求进行自定义配置,以满足不同的测试场景。
四、基本使用
4.1 测试文件命名规则
Jest 默认会查找符合特定命名规则的测试文件,常见的命名规则为[文件名].test.js或[文件名].spec.js。例如,要测试math.js文件,可创建math.test.js或math.spec.js。
4.2 编写测试用例
- 测试函数:使用test函数来定义一个测试用例,test函数接受两个参数:测试用例的描述和测试函数。
test('adds 1 + 2 to equal 3', () => {
// 测试逻辑
});
- 断言函数:使用expect函数来进行断言,expect函数接受一个实际值,然后通过调用各种匹配器(如toBe、toEqual等)来判断实际值是否符合预期。
function add(a, b) {
return a + b;
}
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
4.3 运行测试
在项目根目录下,通过命令行运行测试:
npm test
# 或者
yarn test
如果安装了全局的 Jest,也可以直接运行jest命令。
五、断言与匹配器
5.1 常用断言方法
- toBe:判断两个值是否严格相等(使用===)。
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
- toEqual:用于比较对象或数组的内容是否相等。
test('objects are equal', () => {
const obj1 = { a: 1 };
const obj2 = { a: 1 };
expect(obj1).toEqual(obj2);
});
- toBeTruthy:判断值是否为真。
test('true is truthy', () => {
expect(true).toBeTruthy();
});
- toBeFalsy:判断值是否为假。
test('false is falsy', () => {
expect(false).toBeFalsy();
});
- toBeGreaterThan:判断值是否大于某个值。
test('5 is greater than 3', () => {
expect(5).toBeGreaterThan(3);
});
- toBeLessThan:判断值是否小于某个值。
test('3 is less than 5', () => {
expect(3).toBeLessThan(5);
});
5.2 高级断言技巧
- 嵌套断言:可以在一个断言中嵌套多个断言,以更细粒度地验证复杂数据结构。
test('nested objects are equal', () => {
const obj1 = { a: { b: 1 } };
const obj2 = { a: { b: 1 } };
expect(obj1).toEqual({
a: expect.objectContaining({
b: expect.any(Number)
})
});
});
- 自定义匹配器:Jest 允许开发者自定义匹配器,以满足特定的测试需求。
expect.extend({
toBeEven: (received) => {
const pass = received % 2 === 0;
if (pass) {
return {
message: () => `expected ${received} not to be even`,
pass: true
};
} else {
return {
message: () => `expected ${received} to be even`,
pass: false
};
}
}
});
test('2 is even', () => {
expect(2).toBeEven();
});
六、测试异步代码
6.1 异步函数返回 Promise
- 使用 async/await:通过async/await语法可以方便地测试返回 Promise 的异步函数。
function asyncAdd(a, b) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(a + b);
}, 1000);
});
}
test('async add works', async () => {
const result = await asyncAdd(1, 2);
expect(result).toBe(3);
});
- 使用.then () :也可以使用.then()方法来处理 Promise。
test('async add works with then', () => {
return asyncAdd(1, 2).then((result) => {
expect(result).toBe(3);
});
});
6.2 使用回调函数
- 使用 done () :在测试使用回调函数的异步代码时,需要调用done()方法来通知 Jest 测试完成。
function addWithCallback(a, b, callback) {
setTimeout(() => {
callback(a + b);
}, 1000);
}
test('add with callback works', (done) => {
addWithCallback(1, 2, (result) => {
expect(result).toBe(3);
done();
});
});
6.3 处理异步错误
- 使用 try/catch:在async/await中,可以使用try/catch来捕获异步错误。
test('async function throws error', async () => {
async function asyncError() {
throw new Error('Async error');
}
try {
await asyncError();
} catch (error) {
expect(error.message).toBe('Async error');
}
});
- 使用.rejectWith () :在使用.then()时,可以使用.rejectWith()来处理 Promise 被拒绝的情况。
test('async function throws error with then', () => {
async function asyncError() {
throw new Error('Async error');
}
return asyncError().catch((error) => {
expect(error.message).toBe('Async error');
});
});
七、模拟函数(Mock Functions)
7.1 创建模拟函数
- jest.fn() :使用jest.fn()创建一个模拟函数,模拟函数可以记录调用次数、参数等信息。
const mockFunction = jest.fn();
mockFunction();
expect(mockFunction).toHaveBeenCalled();
- 自定义返回值:可以通过mockReturnValue或mockResolvedValue(用于异步函数)来设置模拟函数的返回值。
const mockFunction = jest.fn().mockReturnValue(42);
const result = mockFunction();
expect(result).toBe(42);
7.2 模拟模块
- jest.mock() :使用jest.mock()来自动模拟一个模块,Jest 会自动生成一个模拟模块,其中所有导出的函数都被替换为模拟函数。
// module.js
export function originalFunction() {
return 'original';
}
// test.js
jest.mock('./module');
const { originalFunction } = require('./module');
test('mocked function', () => {
originalFunction.mockReturnValue('mocked');
expect(originalFunction()).toBe('mocked');
});
- 手动模拟模块:除了自动模拟,也可以手动创建一个模拟模块,然后通过jest.mock()指定使用该模拟模块。
// __mocks__/module.js
export function originalFunction() {
return'mocked';
}
// test.js
jest.mock('./module');
const { originalFunction } = require('./module');
test('manually mocked function', () => {
expect(originalFunction()).toBe('mocked');
});
7.3 模拟函数的使用场景
- 隔离测试:通过模拟依赖项,将测试对象与外部依赖隔离开来,确保测试的独立性和准确性。
- 测试异步操作:模拟异步操作的结果,以便在测试中控制和验证异步行为。
- 测试边缘情况:通过模拟不同的输入和返回值,测试函数在各种边缘情况下的表现。
八、快照测试
8.1 基本原理与使用方法
- 原理:快照测试通过生成一个包含代码输出的快照文件,在后续测试中对比实际输出与快照文件的内容,从而判断代码的行为是否发生变化。
- 使用方法:使用toMatchSnapshot()匹配器来进行快照测试。
function formatUser(user) {
return `Name: ${user.name}, Age: ${user.age}`;
}
test('format user snapshot', () => {
const user = { name: 'John', age: 30 };
expect(formatUser(user)).toMatchSnapshot();
});
8.2 快照文件管理
- 生成快照文件:首次运行快照测试时,Jest 会在__snapshots__目录下生成一个快照文件,文件名与测试文件相对应。
- 更新快照文件:当代码发生变化,需要更新快照文件时,可以使用jest --updateSnapshot命令或在测试运行时按下u键。
- 删除快照文件:如果不再需要某个快照文件,可以手动删除__snapshots__目录下对应的文件。
8.3 快照测试的应用场景
- UI 组件测试:在测试 React、Vue 等前端框架的组件时,快照测试可以方便地验证组件的渲染结果是否符合预期。
- 数据格式化测试:对于数据格式化函数,快照测试可以确保数据格式化的结果在代码修改后保持一致。
- API 响应测试:在测试 API 调用时,快照测试可以验证 API 响应的结构和内容是否发生变化。
九、测试覆盖率
9.1 测试覆盖率的概念与意义
测试覆盖率是指测试代码对被测试代码的覆盖程度,它是衡量测试质量的一个重要指标。较高的测试覆盖率意味着更多的代码被测试到,从而降低了代码中存在未被发现的缺陷的风险。通过分析测试覆盖率报告,开发者可以了解哪些代码没有被充分测试,进而针对性地编写测试用例,提高测试的全面性和有效性。
9.2 Jest 的测试覆盖率工具
Jest 内置了测试覆盖率工具,通过--coverage参数可以生成测试覆盖率报告。在运行测试时,添加--coverage参数:
npm test -- --coverage
# 或者
yarn test -- --coverage
Jest 会在项目根目录下生成一个coverage目录,其中包含详细的测试覆盖率报告,包括 HTML 格式和文本格式的报告。
9.3 覆盖率报告解读
- 语句覆盖率:表示被执行的语句占总语句数的比例。
- 分支覆盖率:表示被执行的分支(如if-else语句、switch语句等)占总分支数的比例。
- 函数覆盖率:表示被调用的函数占总函数数的比例。
- 行覆盖率:表示被执行的代码行占总行数的比例。
9.4 提高测试覆盖率的方法
- 补充测试用例:根据覆盖率报告,找出未被覆盖的代码部分,编写相应的测试用例。
- 优化测试策略:合理设计测试用例,确保覆盖各种可能的输入和场景,避免测试用例的重复和冗余。
- 使用条件覆盖:对于复杂的条件语句,使用条件覆盖策略,确保每个条件分支都被测试到。
十、Jest 在不同场景下的应用
10.1 前端开发中的应用
- React 组件测试:使用 Jest 结合 React Testing Library 或 Enzyme,可以方便地测试 React 组件的渲染、交互和状态变化。
import React from'react';
import { render, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
test('renders MyComponent and clicks button', () => {
const { getByText } = render(<MyComponent />);
const button = getByText('Click me');
fireEvent.click(button);
// 断言组件状态变化
});
- Vue 组件测试:对于 Vue 组件,可以使用 Jest 结合 Vue Test Utils 进行测试。
import { mount } from '@vue/test-utils';
import MyVueComponent from './MyVueComponent.vue';
test('renders Vue component and updates data', () => {
const wrapper = mount(MyVueComponent);
wrapper.find('button').trigger('click');
// 断言组件数据变化
});
10.2 后端开发中的应用
- Node.js 应用测试:在 Node.js 应用开发中,Jest 可以用于测试路由、中间件、数据库操作等功能。
const request = require('supertest');
const app = require('./app');
test('GET / returns 200', async () => {
const response = await request(app).get('/
10.2 后端开发中的应用(续)
- Node.js 应用测试:
-
- 路由测试:在测试路由时,除了验证响应状态码,还可以检查响应体的内容。例如,假设我们有一个简单的 Express 应用,包含一个返回用户列表的路由。
const express = require('express');
const app = express();
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
app.get('/users', (req, res) => {
res.json(users);
});
const request = require('supertest');
test('GET /users returns users list', async () => {
const response = await request(app).get('/users');
expect(response.status).toBe(200);
expect(response.body).toEqual(users);
});
- 中间件测试:中间件在 Node.js 应用中起着至关重要的作用,比如验证用户身份、记录日志等。以一个简单的身份验证中间件为例,使用 Jest 测试它是否能正确拦截未授权的请求。
const express = require('express');
const app = express();
// 模拟身份验证中间件
const authMiddleware = (req, res, next) => {
const authHeader = req.headers['authorization'];
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
// 这里简单模拟token验证
if (token === 'validToken') {
return next();
}
}
res.status(401).send('Unauthorized');
};
app.get('/protected', authMiddleware, (req, res) => {
res.send('Protected resource');
});
const request = require('supertest');
test('GET /protected rejects unauthenticated requests', async () => {
const response = await request(app).get('/protected');
expect(response.status).toBe(401);
expect(response.text).toBe('Unauthorized');
});
test('GET /protected allows authenticated requests', async () => {
const response = await request(app)
.get('/protected')
.set('Authorization', 'Bearer validToken');
expect(response.status).toBe(200);
expect(response.text).toBe('Protected resource');
});
- 数据库操作测试:对于涉及数据库操作的函数,利用 Jest 的模拟函数来隔离测试。以使用mysql模块进行数据库查询为例。
const mysql = require('mysql');
// 模拟数据库连接池
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'testdb'
});
// 假设一个获取用户信息的函数
function getUserById(id, callback) {
const query = 'SELECT * FROM users WHERE id =?';
pool.query(query, [id], (error, results) => {
if (error) {
return callback(error);
}
callback(null, results[0]);
});
}
// 测试文件
const { getUserById } = require('./userService');
const mysql = require('mysql');
jest.mock('mysql');
test('getUserById retrieves user from database', (done) => {
const mockResults = [{ id: 1, name: 'Alice' }];
const mockQuery = jest.fn((sql, values, cb) => {
cb(null, mockResults);
});
mysql.createPool.mockReturnValue({
query: mockQuery
});
getUserById(1, (error, user) => {
expect(error).toBe(null);
expect(user).toEqual(mockResults[0]);
done();
});
});
10.3 测试 GraphQL API
随着 GraphQL 在前后端开发中的广泛应用,使用 Jest 测试 GraphQL API 也变得越来越重要。以express - graphql为例,展示如何使用 Jest 测试 GraphQL 的查询和变更。
const express = require('express');
const { buildSchema } = require('graphql');
const { graphqlHTTP } = require('express - graphql');
// 构建GraphQL schema
const schema = buildSchema(`
type Query {
hello: String
}
`);
// 定义解析器
const root = {
hello: () => 'Hello world!'
};
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true // 开启GraphiQL调试工具
}));
const request = require('supertest');
test('GraphQL query hello returns "Hello world!"', async () => {
const response = await request(app)
.post('/graphql')
.send({ query: '{ hello }' });
expect(response.status).toBe(200);
expect(response.body.data.hello).toBe('Hello world!');
});
10.4 测试工具与库的结合使用
在实际项目中,Jest 常常与其他工具和库结合使用,以提高测试效率和质量。
- Sinon.js:Sinon.js 是一个功能强大的 JavaScript 测试库,提供了丰富的模拟、桩(stub)和间谍(spy)功能。与 Jest 结合使用,可以更灵活地控制测试环境。例如,在测试一个依赖外部 API 的函数时,使用 Sinon 的stub来模拟 API 调用,而不是实际发送请求。
const sinon = require('sinon');
const axios = require('axios');
const myFunction = require('./myFunction');
test('myFunction calls external API correctly', async () => {
const mockResponse = { data: { message: 'Mocked response' } };
const stub = sinon.stub(axios, 'get').resolves(mockResponse);
const result = await myFunction();
expect(result).toEqual(mockResponse.data);
stub.restore();
});
- Cypress:Cypress 是一个用于前端自动化测试的工具,专注于浏览器端的交互测试。虽然 Jest 主要用于单元测试,但在一些场景下,结合 Cypress 可以实现更全面的测试覆盖。例如,使用 Jest 进行组件的单元测试,使用 Cypress 进行端到端的集成测试,确保整个应用在浏览器环境中的行为符合预期。
10.5 持续集成中的 Jest
在持续集成(CI)环境中,Jest 扮演着关键角色,确保每次代码提交都经过充分的测试。以 GitHub Actions 为例,展示如何在 CI 环境中配置 Jest 测试。
name: Jest Tests
on:
push:
branches:
- main
jobs:
build:
runs - on: ubuntu - latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup - node@v2
with:
node - version: '14'
- name: Install dependencies
run: npm install
- name: Run Jest tests
run: npm test -- --coverage
通过上述配置,每次代码推送到main分支时,GitHub Actions 会自动拉取代码,安装依赖,然后运行 Jest 测试,并生成测试覆盖率报告。这有助于及时发现代码中的问题,保证项目的稳定性和质量。
10.6 Jest 在微服务架构中的应用
在微服务架构中,各个服务相互独立又协同工作。Jest 可以用于对每个微服务进行单元测试和集成测试,确保服务的可靠性和稳定性。
10.6.1 服务接口测试
每个微服务都会暴露一些接口供其他服务调用。使用 Jest 结合supertest等库,可以测试这些接口的正确性。例如,假设我们有一个用户管理微服务,提供了获取用户信息的接口。
const express = require('express');
const app = express();
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
app.get('/users/:id', (req, res) => {
const userId = parseInt(req.params.id);
const user = users.find(u => u.id === userId);
if (user) {
res.json(user);
} else {
res.status(404).send('User not found');
}
});
const request = require('supertest');
test('GET /users/:id returns correct user', async () => {
const response = await request(app).get('/users/1');
expect(response.status).toBe(200);
expect(response.body).toEqual(users[0]);
});
test('GET /users/:id returns 404 for non - existent user', async () => {
const response = await request(app).get('/users/3');
expect(response.status).toBe(404);
expect(response.text).toBe('User not found');
});
10.6.2 服务间通信测试
微服务之间通常通过 HTTP、消息队列等方式进行通信。在测试时,可以使用 Jest 模拟其他服务的响应,测试当前服务在不同通信场景下的行为。例如,使用nock库来模拟 HTTP 请求的响应,测试一个依赖其他服务获取数据的微服务。
const axios = require('axios');
const nock = require('nock');
const myService = require('./myService');
test('myService calls other service and processes response', async () => {
const mockData = { message: 'Mocked data from other service' };
nock('http://other - service.com')
.get('/data')
.reply(200, mockData);
const result = await myService.fetchData();
expect(result).toEqual(mockData);
});
10.7 Jest 与测试报告工具的整合
为了更直观地展示测试结果,Jest 可以与一些测试报告工具进行整合。
10.7.1 Jest 与 Allure 的整合
Allure 是一个功能强大的测试报告工具,它可以生成美观、详细的测试报告。通过jest - allure - reporter插件,可以将 Jest 的测试结果集成到 Allure 报告中。
- 安装插件:
npm install --save - dev jest - allure - reporter
- 在jest.config.js中配置:
module.exports = {
// 其他配置项
reporters: [
'default',
'jest - allure - reporter'
]
};
- 运行测试后,在项目根目录下会生成allure - results目录,使用 Allure 命令行工具可以生成报告:
allure serve allure - results
生成的报告中会包含每个测试用例的详细信息,如执行时间、状态、断言结果等,方便团队成员查看和分析测试结果。
10.7.2 Jest 与 Cucumber 的整合(BDD 风格测试)
Cucumber 是一个支持行为驱动开发(BDD)的工具,它使用自然语言描述测试场景。通过cucumber - jest库,可以将 Jest 与 Cucumber 结合,实现 BDD 风格的测试。
- 安装依赖:
npm install --save - dev cucumber - jest cucumber
- 创建features目录,在其中编写 Cucumber 的特性文件(.feature),例如login.feature:
Feature: User Login
As a user
I want to log in to the application
So that I can access my account
Scenario: Successful login
Given I am on the login page
When I enter my username "testuser" and password "testpass"
And I click the login button
Then I should be redirected to my account page
- 创建相应的步骤定义文件(.js),在其中使用 Jest 的断言实现每个步骤的逻辑:
const { Given, When, Then } = require('cucumber - jest');
const { render, fireEvent } = require('@testing - library/react');
const LoginPage = require('./LoginPage');
Given('I am on the login page', () => {
// 渲染登录页面组件
const { container } = render(<LoginPage />);
// 可以进行一些初始化操作,如保存页面容器
});
When('I enter my username "testuser" and password "testpass"', () => {
// 找到用户名和密码输入框,模拟输入
const usernameInput = document.getElementsByName('username')[0];
const passwordInput = document.getElementsByName('password')[0];
fireEvent.change(usernameInput, { target: { value: 'testuser' } });
fireEvent.change(passwordInput, { target: { value: 'testpass' } });
});
When('I click the login button', () => {
// 找到登录按钮,模拟点击
const loginButton = document.querySelector('button[type="submit"]');
fireEvent.click(loginButton);
});
Then('I should be redirected to my account page', () => {
// 断言页面是否重定向到正确的URL
expect(window.location.href).toContain('/account');
});
通过这种方式,可以将业务逻辑以自然语言的形式描述出来,使非技术人员也能理解测试的目的和场景,同时利用 Jest 强大的断言和测试功能实现具体的测试逻辑。