跳到主要内容

JavaScript Jest框架

什么是Jest?

Jest是由Facebook开发的一个优秀的JavaScript测试框架,以其零配置、快速执行、内置代码覆盖率报告等特性受到广泛欢迎。Jest特别适合用于测试React应用,但也可以用于测试任何JavaScript代码。

备注

Jest被设计为"开箱即用",让开发者可以专注于编写测试而不是配置测试环境。

为什么需要Jest?

在构建JavaScript应用时,单元测试是确保代码质量的重要环节。Jest提供了以下优势:

  • 简单的配置:几乎零配置即可开始测试
  • 快速:并行运行测试,提高测试效率
  • 内置代码覆盖率报告:无需额外工具
  • 快照测试:轻松测试UI组件
  • 模拟功能:强大的mock能力

安装Jest

在项目中安装Jest非常简单:

bash
npm install --save-dev jest

然后在package.json中添加测试脚本:

json
{
"scripts": {
"test": "jest"
}
}

Jest基础用法

编写你的第一个测试

让我们从一个简单的示例开始:

  1. 首先创建一个要测试的函数:
javascript
// sum.js
function sum(a, b) {
return a + b;
}

module.exports = sum;
  1. 然后为这个函数创建测试:
javascript
// sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
  1. 运行测试:
bash
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)用于测试值:

javascript
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

javascript
// fetchData.js
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('peanut butter');
}, 100);
});
}

module.exports = fetchData;
javascript
// fetchData.test.js
const fetchData = require('./fetchData');

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

使用async/await

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

测试设置和拆卸

Jest提供了几个函数来帮助你在测试前后进行设置和清理:

javascript
// 所有测试之前运行一次
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允许你模拟函数、模块和依赖,这在单元测试中特别有用:

javascript
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);
});

模拟返回值

javascript
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的函数:

javascript
// users.js
const axios = require('axios');

class Users {
static all() {
return axios.get('/users').then(resp => resp.data);
}
}

module.exports = Users;

我们可以模拟axios来测试这个函数:

javascript
// 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组件特别有用:

javascript
// Link.js
const Link = ({page, children}) => {
return (
<a href={page || '#'}>
{children}
</a>
);
};

export default Link;

快照测试示例:

javascript
// 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会将渲染结果与快照进行比较。

实际项目案例:计算器应用

假设我们在构建一个简单的计算器应用:

javascript
// 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;

为这个计算器编写全面的测试:

javascript
// 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标志:

bash
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应用,你需要安装额外的包:

bash
npm install --save-dev @testing-library/react @testing-library/jest-dom

测试一个简单的React组件:

javascript
// Button.js
import React from 'react';

function Button({onClick, children}) {
return (
<button onClick={onClick}>
{children}
</button>
);
}

export default Button;
javascript
// 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:

bash
npm install --save-dev @vue/test-utils vue-jest

测试一个简单的Vue组件:

javascript
// 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>
javascript
// 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调用

javascript
// api.js
import axios from 'axios';

export const fetchUser = (id) => {
return axios.get(`/users/${id}`).then(response => response.data);
};
javascript
// 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');
});

定时器测试

javascript
// timer.js
export function delayedCallback(callback, delay) {
return setTimeout(callback, delay);
}
javascript
// 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可以帮助你维护高质量的代码库,并有信心进行重构和添加新功能。

练习

  1. 为一个计算文本字符数的函数编写测试
  2. 编写测试验证一个异步函数的成功和失败情况
  3. 使用Jest模拟一个第三方API调用
  4. 为一个简单的React/Vue组件编写测试

扩展资源

提示

编写测试可能一开始会感觉繁琐,但随着项目的发展,测试会为你节省大量时间,并帮助你维护更健壮的代码。