JavaScript Jest框架
什么是Jest?
Jest是由Facebook开发的一个优秀的JavaScript测试框架,以其零配置、快速执行、内置代码覆盖率报告等特性受到广泛欢迎。Jest特别适合用于测试React应用,但也可以用于测试任何JavaScript代码。
Jest被设计为"开箱即用",让开发者可以专注于编写测试而不是配置测试环境。
为什么需要Jest?
在构建JavaScript应用时,单元测试是确保代码质量的重要环节。Jest提供了以下优势:
- 简单的配置:几乎零配置即可开始测试
- 快速:并行运行测试,提高测试效率
- 内置代码覆盖率报告:无需额外工具
- 快照测试:轻松测试UI组件
- 模拟功能:强大的mock能力
安装Jest
在项目中安装Jest非常简单:
npm install --save-dev jest
然后在package.json
中添加测试脚本:
{
"scripts": {
"test": "jest"
}
}
Jest基础用法
编写你的第一个测试
让我们从一个简单的示例开始:
- 首先创建一个要测试的函数:
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
- 然后为这个函数创建测试:
// sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
- 运行测试:
npm test
你应该看到类似以下的输出:
PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.128s
Jest匹配器
Jest提供了多种匹配器(matchers)用于测试值:
test('常用匹配器示例', () => {
// 精确相等
expect(2 + 2).toBe(4);
// 对象相等(递归比较对象的所有属性)
expect({ name: 'Jack' }).toEqual({ name: 'Jack' });
// 相反匹配
expect(2 + 2).not.toBe(5);
// 真值检查
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(true).toBeTruthy();
expect(false).toBeFalsy();
// 数字比较
expect(4).toBeGreaterThan(3);
expect(4).toBeLessThan(5);
// 字符串匹配
expect('Hello world').toMatch(/world/);
// 数组包含
expect([1, 2, 3]).toContain(2);
});
测试异步代码
测试Promise
// fetchData.js
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('peanut butter');
}, 100);
});
}
module.exports = fetchData;
// fetchData.test.js
const fetchData = require('./fetchData');
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
使用async/await
test('the data is peanut butter (async/await)', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
测试设置和拆卸
Jest提供了几个函数来帮助你在测试前后进行设置和清理:
// 所有测试之前运行一次
beforeAll(() => {
console.log('Setup before all tests');
});
// 每个测试之前运行
beforeEach(() => {
console.log('Setup before each test');
});
// 每个测试之后运行
afterEach(() => {
console.log('Cleanup after each test');
});
// 所有测试之后运行一次
afterAll(() => {
console.log('Cleanup after all tests');
});
test('test 1', () => {
console.log('Test 1 running');
expect(true).toBeTruthy();
});
test('test 2', () => {
console.log('Test 2 running');
expect(true).toBeTruthy();
});
模拟函数
Jest允许你模拟函数、模块和依赖,这在单元测试中特别有用:
test('模拟函数示例', () => {
// 创建一个模拟函数
const mockCallback = jest.fn();
// 使用模拟函数
mockCallback(1);
mockCallback(2);
// 验证调用次数
expect(mockCallback.mock.calls.length).toBe(2);
// 验证第一次调用的参数
expect(mockCallback.mock.calls[0][0]).toBe(1);
// 验证第二次调用的参数
expect(mockCallback.mock.calls[1][0]).toBe(2);
});
模拟返回值
test('模拟返回值', () => {
const myMock = jest.fn();
// 设置返回值
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
expect(myMock()).toBe(10);
expect(myMock()).toBe('x');
expect(myMock()).toBe(true);
expect(myMock()).toBe(true);
});
模拟模块
假设我们有一个依赖于axios的函数:
// users.js
const axios = require('axios');
class Users {
static all() {
return axios.get('/users').then(resp => resp.data);
}
}
module.exports = Users;
我们可以模拟axios来测试这个函数:
// users.test.js
const axios = require('axios');
const Users = require('./users');
// 模拟axios模块
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
// 设置axios.get的模拟返回值
axios.get.mockResolvedValue(resp);
// 调用我们要测试的方法
return Users.all().then(data => {
expect(data).toEqual(users);
});
});
快照测试
快照测试对于UI组件特别有用:
// Link.js
const Link = ({page, children}) => {
return (
<a href={page || '#'}>
{children}
</a>
);
};
export default Link;
快照测试示例:
// Link.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import Link from './Link';
test('Link renders correctly', () => {
const tree = renderer
.create(<Link page="https://example.com">Example</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
第一次运行时,Jest会创建一个快照文件。后续运行时,Jest会将渲染结果与快照进行比较。
实际项目案例:计算器应用
假设我们在构建一个简单的计算器应用:
// calculator.js
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
}
module.exports = Calculator;
为这个计算器编写全面的测试:
// calculator.test.js
const Calculator = require('./calculator');
// 使用describe分组相关测试
describe('Calculator', () => {
let calculator;
// 每个测试前创建新的计算器实例
beforeEach(() => {
calculator = new Calculator();
});
// 加法测试
describe('add method', () => {
test('should add two positive numbers correctly', () => {
expect(calculator.add(2, 3)).toBe(5);
});
test('should handle negative numbers', () => {
expect(calculator.add(-1, 1)).toBe(0);
expect(calculator.add(-1, -1)).toBe(-2);
});
test('should handle decimal numbers', () => {
expect(calculator.add(0.1, 0.2)).toBeCloseTo(0.3);
});
});
// 减法测试
describe('subtract method', () => {
test('should subtract numbers correctly', () => {
expect(calculator.subtract(5, 2)).toBe(3);
});
test('should handle negative results', () => {
expect(calculator.subtract(2, 5)).toBe(-3);
});
});
// 乘法测试
describe('multiply method', () => {
test('should multiply numbers correctly', () => {
expect(calculator.multiply(2, 3)).toBe(6);
});
test('should handle zero', () => {
expect(calculator.multiply(5, 0)).toBe(0);
});
test('should handle negative numbers', () => {
expect(calculator.multiply(-2, 3)).toBe(-6);
expect(calculator.multiply(-2, -3)).toBe(6);
});
});
// 除法测试
describe('divide method', () => {
test('should divide numbers correctly', () => {
expect(calculator.divide(6, 2)).toBe(3);
});
test('should throw error when dividing by zero', () => {
expect(() => calculator.divide(5, 0)).toThrow("Cannot divide by zero");
});
});
});
这个例子展示了如何:
- 使用
describe
组织测试 - 使用
beforeEach
设置测试环境 - 测试多种边界条件
- 测试错误抛出
Jest测试覆盖率
Jest内置了代码覆盖率报告功能,只需在运行测试时添加--coverage
标志:
npm test -- --coverage
这将生成详细的覆盖率报告,显示:
- 语句覆盖率
- 分支覆盖率
- 函数覆盖率
- 行覆盖率
覆盖率报告示例:
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
calculator.js | 100 | 100 | 100 | 100 |
-----------------|---------|----------|---------|---------|-------------------
Jest和前端框架集成
Jest与流行的前端框架结合使用非常简单:
React
对于React应用,你需要安装额外的包:
npm install --save-dev @testing-library/react @testing-library/jest-dom
测试一个简单的React组件:
// Button.js
import React from 'react';
function Button({onClick, children}) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
export default Button;
// Button.test.js
import React from 'react';
import {render, fireEvent} from '@testing-library/react';
import '@testing-library/jest-dom';
import Button from './Button';
test('calls onClick prop when clicked', () => {
const handleClick = jest.fn();
const {getByText} = render(
<Button onClick={handleClick}>Click Me</Button>
);
fireEvent.click(getByText('Click Me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
Vue
对于Vue应用,可以使用Vue Test Utils:
npm install --save-dev @vue/test-utils vue-jest
测试一个简单的Vue组件:
// Counter.vue
<template>
<div>
<span class="count">{{ count }}</span>
<button @click="increment">Increment</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count += 1
}
}
}
</script>
// Counter.test.js
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter', () => {
test('increments count when button is clicked', async () => {
const wrapper = mount(Counter)
const button = wrapper.find('button')
expect(wrapper.find('.count').text()).toBe('0')
await button.trigger('click')
expect(wrapper.find('.count').text()).toBe('1')
})
})
常见测试模式
测试API调用
// api.js
import axios from 'axios';
export const fetchUser = (id) => {
return axios.get(`/users/${id}`).then(response => response.data);
};
// api.test.js
import axios from 'axios';
import { fetchUser } from './api';
jest.mock('axios');
test('fetches user data successfully', async () => {
const user = { id: 1, name: 'John' };
axios.get.mockResolvedValue({ data: user });
const data = await fetchUser(1);
expect(data).toEqual(user);
expect(axios.get).toHaveBeenCalledWith('/users/1');
});
定时器测试
// timer.js
export function delayedCallback(callback, delay) {
return setTimeout(callback, delay);
}
// timer.test.js
import { delayedCallback } from './timer';
jest.useFakeTimers();
test('calls callback after 1 second', () => {
const callback = jest.fn();
delayedCallback(callback, 1000);
// 在这一点上,回调不应该被调用
expect(callback).not.toBeCalled();
// 快进时间
jest.advanceTimersByTime(1000);
// 现在回调应该被调用
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
总结
Jest是一个功能强大的JavaScript测试框架,适用于各种类型的项目:
- 它提供了丰富的匹配器,可以测试各种值和情况
- 支持异步代码测试
- 强大的模拟功能让你可以隔离测试单元
- 内置快照测试和代码覆盖率报告
- 与主流前端框架集成良好
随着项目规模的增长,自动化测试变得越来越重要。使用Jest可以帮助你维护高质量的代码库,并有信心进行重构和添加新功能。
练习
- 为一个计算文本字符数的函数编写测试
- 编写测试验证一个异步函数的成功和失败情况
- 使用Jest模拟一个第三方API调用
- 为一个简单的React/Vue组件编写测试
扩展资源
编写测试可能一开始会感觉繁琐,但随着项目的发展,测试会为你节省大量时间,并帮助你维护更健壮的代码。