Jest 教程

1. 核心知识点讲解

1.1 Jest 简介

Jest 是 Facebook 开发的一个功能丰富的 JavaScript 测试框架,专注于简洁性和易用性。它提供了完整的测试解决方案,包括单元测试、集成测试和快照测试,并且内置了 Mock 功能和测试覆盖率报告。

1.2 安装和配置

安装 Jest:

npm install --save-dev jest

配置 package.json:

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

创建 jest.config.js 配置文件:

module.exports = {
  testEnvironment: 'node',
  coverageDirectory: 'coverage',
  collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
  testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)']
};

1.3 基本测试结构

测试文件命名约定:

  • __tests__ 目录下的文件
  • .test.js.spec.js 结尾的文件

基本测试结构:

test('测试描述', () => {
  // 测试代码
  expect(1 + 1).toBe(2);
});

// 或使用 it 别名
it('测试描述', () => {
  expect(1 + 1).toBe(2);
});

1.4 断言方法

Jest 提供了丰富的断言方法:

  • expect(value).toBe(expected) - 严格相等
  • expect(value).toEqual(expected) - 深度相等
  • expect(value).toBeTruthy() - 真值
  • expect(value).toBeFalsy() - 假值
  • expect(value).toBeNull() - 为 null
  • expect(value).toBeUndefined() - 为 undefined
  • expect(value).toBeDefined() - 已定义
  • expect(value).toContain(item) - 包含指定项
  • expect(value).toMatch(regexp) - 匹配正则表达式
  • expect(value).toThrow(error) - 抛出错误

1.5 测试生命周期

测试钩子:

describe('测试套件', () => {
  beforeAll(() => {
    // 在所有测试开始前运行
  });

  afterAll(() => {
    // 在所有测试结束后运行
  });

  beforeEach(() => {
    // 在每个测试开始前运行
  });

  afterEach(() => {
    // 在每个测试结束后运行
  });

  test('测试用例 1', () => {
    // 测试代码
  });

  test('测试用例 2', () => {
    // 测试代码
  });
});

1.6 Mock 功能

Mock 函数:

const mockFn = jest.fn();
mockFn.mockReturnValue('默认返回值');
mockFn.mockResolvedValue('异步返回值');
mockFn.mockRejectedValue(new Error('错误信息'));

Mock 模块:

// __mocks__/fs.js
module.exports = {
  readFileSync: jest.fn(() => '模拟文件内容')
};

// 在测试文件中
jest.mock('fs');
const fs = require('fs');

test('测试读取文件', () => {
  expect(fs.readFileSync('test.txt')).toBe('模拟文件内容');
});

1.7 快照测试

快照测试:

test('快照测试', () => {
  const component = {
    name: '测试组件',
    props: { value: 123 }
  };
  expect(component).toMatchSnapshot();
});

更新快照:

npm test -- -u

1.8 测试覆盖率

运行测试并生成覆盖率报告:

npm test -- --coverage

覆盖率报告指标:

  • 语句覆盖率 (Statement Coverage)
  • 分支覆盖率 (Branch Coverage)
  • 函数覆盖率 (Function Coverage)
  • 行覆盖率 (Line Coverage)

2. 实用案例分析

2.1 基本测试案例

场景: 测试一个简单的计算器函数

实现:

// src/calculator.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('除数不能为零');
  }
  return a / b;
}

module.exports = {
  add,
  subtract,
  multiply,
  divide
};

// src/__tests__/calculator.test.js
const { add, subtract, multiply, divide } = require('../calculator');

describe('计算器测试', () => {
  test('加法测试', () => {
    expect(add(1, 2)).toBe(3);
    expect(add(-1, 1)).toBe(0);
    expect(add(0, 0)).toBe(0);
  });

  test('减法测试', () => {
    expect(subtract(5, 3)).toBe(2);
    expect(subtract(1, 5)).toBe(-4);
    expect(subtract(0, 0)).toBe(0);
  });

  test('乘法测试', () => {
    expect(multiply(2, 3)).toBe(6);
    expect(multiply(-1, 5)).toBe(-5);
    expect(multiply(0, 100)).toBe(0);
  });

  test('除法测试', () => {
    expect(divide(6, 2)).toBe(3);
    expect(divide(5, 2)).toBe(2.5);
    expect(divide(-10, 2)).toBe(-5);
  });

  test('除法测试 - 除数为零', () => {
    expect(() => divide(1, 0)).toThrow('除数不能为零');
  });
});

2.2 Mock 测试案例

场景: 测试一个依赖外部 API 的函数

实现:

// src/api.js
const axios = require('axios');

async function fetchUser(userId) {
  const response = await axios.get(`https://api.example.com/users/${userId}`);
  return response.data;
}

module.exports = { fetchUser };

// src/__tests__/api.test.js
const axios = require('axios');
const { fetchUser } = require('../api');

// Mock axios
jest.mock('axios');

describe('API 测试', () => {
  test('获取用户信息成功', async () => {
    // 模拟响应数据
    const mockUser = { id: 1, name: '张三' };
    axios.get.mockResolvedValue({ data: mockUser });

    // 调用被测试函数
    const user = await fetchUser(1);

    // 验证结果
    expect(user).toEqual(mockUser);
    expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
    expect(axios.get).toHaveBeenCalledTimes(1);
  });

  test('获取用户信息失败', async () => {
    // 模拟错误响应
    const errorMessage = '网络错误';
    axios.get.mockRejectedValue(new Error(errorMessage));

    // 验证错误
    await expect(fetchUser(1)).rejects.toThrow(errorMessage);
    expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
  });
});

2.3 快照测试案例

场景: 测试一个生成 HTML 的函数

实现:

// src/htmlGenerator.js
function generateUserCard(user) {
  return `
    <div class="user-card">
      <h2>${user.name}</h2>
      <p>邮箱: ${user.email}</p>
      <p>年龄: ${user.age}</p>
    </div>
  `;
}

module.exports = { generateUserCard };

// src/__tests__/htmlGenerator.test.js
const { generateUserCard } = require('../htmlGenerator');

describe('HTML 生成器测试', () => {
  test('生成用户卡片 HTML', () => {
    const user = {
      name: '张三',
      email: 'zhangsan@example.com',
      age: 30
    };

    // 快照测试
    expect(generateUserCard(user)).toMatchSnapshot();
  });
});

3. 代码示例

3.1 完整的测试项目结构

项目结构:

src/
├── calculator.js
├── api.js
├── htmlGenerator.js
└── __tests__/
    ├── calculator.test.js
    ├── api.test.js
    └── htmlGenerator.test.js

3.2 运行测试

运行所有测试:

npm test

运行特定测试文件:

npm test src/__tests__/calculator.test.js

运行测试并生成覆盖率报告:

npm test -- --coverage

更新快照:

npm test -- -u

3.3 测试配置示例

jest.config.js:

module.exports = {
  // 测试环境
  testEnvironment: 'node',
  
  // 测试文件匹配模式
  testMatch: [
    '**/__tests__/**/*.[jt]s?(x)',
    '**/?(*.)+(spec|test).[tj]s?(x)'
  ],
  
  // 测试文件路径忽略模式
  testPathIgnorePatterns: [
    '/node_modules/',
    '/coverage/'
  ],
  
  // 模块名称映射
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  
  // 覆盖率报告配置
  coverageDirectory: 'coverage',
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js'
  ],
  
  // 覆盖率阈值
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  
  // 测试超时时间
  testTimeout: 5000,
  
  // 显示测试结果的详细程度
  verbose: true
};

3.4 高级测试技巧

测试异步代码:

// 方法 1: 使用 async/await
test('异步测试 - async/await', async () => {
  const promise = Promise.resolve('成功');
  expect(await promise).toBe('成功');
});

// 方法 2: 使用 .resolves/.rejects
test('异步测试 - .resolves', () => {
  const promise = Promise.resolve('成功');
  expect(promise).resolves.toBe('成功');
});

test('异步测试 - .rejects', () => {
  const promise = Promise.reject(new Error('失败'));
  expect(promise).rejects.toThrow('失败');
});

// 方法 3: 使用回调
test('异步测试 - 回调', (done) => {
  setTimeout(() => {
    expect(1 + 1).toBe(2);
    done();
  }, 100);
});

测试定时器:

test('定时器测试', () => {
  // 模拟定时器
  jest.useFakeTimers();
  
  const callback = jest.fn();
  setTimeout(callback, 1000);
  
  // 快进定时器
  jest.advanceTimersByTime(1000);
  
  // 验证回调被调用
  expect(callback).toHaveBeenCalledTimes(1);
  
  // 恢复真实定时器
  jest.useRealTimers();
});

测试 DOM 操作:

// src/domUtils.js
function createElement(tagName, text) {
  const element = document.createElement(tagName);
  element.textContent = text;
  return element;
}

function appendElement(parent, child) {
  parent.appendChild(child);
}

module.exports = { createElement, appendElement };

// src/__tests__/domUtils.test.js
const { createElement, appendElement } = require('../domUtils');

describe('DOM 工具测试', () => {
  test('创建元素', () => {
    // 设置 DOM 环境
    const { JSDOM } = require('jsdom');
    const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
    global.document = dom.window.document;
    
    const element = createElement('div', '测试文本');
    expect(element.tagName).toBe('DIV');
    expect(element.textContent).toBe('测试文本');
  });
  
  test('追加元素', () => {
    const { JSDOM } = require('jsdom');
    const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
    global.document = dom.window.document;
    
    const parent = document.createElement('div');
    const child = document.createElement('span');
    child.textContent = '子元素';
    
    appendElement(parent, child);
    expect(parent.children.length).toBe(1);
    expect(parent.firstChild).toBe(child);
  });
});

4. 总结

Jest 是一个功能强大的 JavaScript 测试框架,提供了完整的测试解决方案,包括:

  • 简洁易用的测试语法
  • 内置的 Mock 功能
  • 快照测试
  • 测试覆盖率报告
  • 异步测试支持
  • 定时器模拟

通过学习本教程,你可以开始在项目中使用 Jest 编写测试,提高代码质量和可靠性。良好的测试实践可以帮助你:

  • 及早发现并修复 bug
  • 提高代码的可维护性
  • 促进团队协作
  • 减少重构风险

随着实践的深入,你会发现测试不仅是保证代码质量的重要手段,也是提高开发效率的有效工具。

5. 进一步学习资源

希望本教程对你学习 Jest 有所帮助!

« 上一篇 TypeScript 教程 下一篇 » Mocha 教程