Jest测试模式Skill jest-testing-patterns

Jest测试模式技能专注于使用Jest框架进行全面的软件测试,包括单元测试、模拟函数、间谍方法、快照测试和断言技术。适用于前端开发、后端开发等领域的测试覆盖,提升代码质量和可维护性。关键词:Jest、测试、单元测试、模拟、间谍、快照、断言、前端测试、后端测试、自动化测试。

测试 0 次安装 0 次浏览 更新于 3/25/2026

名称: jest-testing-patterns 用户可调用: false 描述: 当需要Jest测试模式时使用,包括单元测试、模拟、间谍、快照和断言技术,以实现全面的测试覆盖。 允许工具: [读取, 写入, 编辑, Bash, Glob, Grep]

Jest测试模式

掌握Jest测试模式,包括单元测试、模拟、间谍、快照和断言技术,以实现全面的测试覆盖。这个技能涵盖了使用Jest编写有效、可维护测试的基本模式和实践。

基本测试结构

测试套件组织

describe('Calculator', () => {
  describe('add', () => {
    it('应该添加两个正数', () => {
      expect(add(2, 3)).toBe(5);
    });

    it('应该添加负数', () => {
      expect(add(-2, -3)).toBe(-5);
    });

    it('应该处理零', () => {
      expect(add(0, 5)).toBe(5);
    });
  });

  describe('subtract', () => {
    it('应该减去两个数', () => {
      expect(subtract(5, 3)).toBe(2);
    });
  });
});

设置和清理

describe('数据库操作', () => {
  let db;

  // 在所有测试之前运行一次
  beforeAll(async () => {
    db = await initializeDatabase();
  });

  // 在所有测试之后运行一次
  afterAll(async () => {
    await db.close();
  });

  // 在每个测试之前运行
  beforeEach(() => {
    db.clear();
  });

  // 在每个测试之后运行
  afterEach(() => {
    db.resetMocks();
  });

  it('应该插入一条记录', async () => {
    const result = await db.insert({ name: 'John' });
    expect(result.id).toBeDefined();
  });

  it('应该找到一条记录', async () => {
    await db.insert({ id: 1, name: 'John' });
    const result = await db.findById(1);
    expect(result.name).toBe('John');
  });
});

匹配器和断言

常见匹配器

describe('匹配器', () => {
  it('应该测试相等性', () => {
    expect(2 + 2).toBe(4); // 严格相等
    expect({ a: 1 }).toEqual({ a: 1 }); // 深度相等
    expect([1, 2, 3]).toStrictEqual([1, 2, 3]); // 严格深度相等
  });

  it('应该测试真值性', () => {
    expect(true).toBeTruthy();
    expect(false).toBeFalsy();
    expect(null).toBeNull();
    expect(undefined).toBeUndefined();
    expect('value').toBeDefined();
  });

  it('应该测试数字', () => {
    expect(4).toBeGreaterThan(3);
    expect(4).toBeGreaterThanOrEqual(4);
    expect(3).toBeLessThan(4);
    expect(3).toBeLessThanOrEqual(3);
    expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
  });

  it('应该测试字符串', () => {
    expect('team').not.toMatch(/I/);
    expect('Christoph').toMatch(/stop/);
    expect('hello world').toContain('world');
  });

  it('应该测试数组和可迭代对象', () => {
    const list = ['apple', 'banana', 'cherry'];
    expect(list).toContain('banana');
    expect(list).toHaveLength(3);
    expect(new Set(list)).toContain('apple');
  });

  it('应该测试对象', () => {
    expect({ a: 1, b: 2 }).toHaveProperty('a');
    expect({ a: 1, b: 2 }).toHaveProperty('a', 1);
    expect({ a: { b: { c: 1 } } }).toHaveProperty('a.b.c', 1);
  });

  it('应该测试异常', () => {
    expect(() => {
      throw new Error('error');
    }).toThrow();
    expect(() => {
      throw new Error('Invalid input');
    }).toThrow('Invalid input');
    expect(() => {
      throw new Error('Invalid input');
    }).toThrow(/Invalid/);
  });
});

异步断言

describe('异步测试', () => {
  // 使用async/await
  it('应该获取数据', async () => {
    const data = await fetchData();
    expect(data).toBeDefined();
  });

  // 使用promises
  it('应该使用promise获取数据', () => {
    return fetchData().then(data => {
      expect(data).toBeDefined();
    });
  });

  // 测试promise拒绝
  it('应该处理错误', async () => {
    await expect(fetchInvalidData()).rejects.toThrow('Not found');
  });

  // 使用resolves/rejects
  it('应该用数据解析', async () => {
    await expect(fetchData()).resolves.toEqual({ id: 1 });
  });

  it('应该用错误拒绝', async () => {
    await expect(fetchInvalidData()).rejects.toThrow();
  });
});

模拟

函数模拟

describe('函数模拟', () => {
  it('应该模拟一个函数', () => {
    const mockFn = jest.fn();
    mockFn('arg1', 'arg2');

    expect(mockFn).toHaveBeenCalled();
    expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
    expect(mockFn).toHaveBeenCalledTimes(1);
  });

  it('应该模拟返回值', () => {
    const mockFn = jest.fn()
      .mockReturnValue(42)
      .mockReturnValueOnce(1)
      .mockReturnValueOnce(2);

    expect(mockFn()).toBe(1);
    expect(mockFn()).toBe(2);
    expect(mockFn()).toBe(42);
  });

  it('应该模拟异步函数', async () => {
    const mockFn = jest.fn()
      .mockResolvedValue('success')
      .mockResolvedValueOnce('first call');

    expect(await mockFn()).toBe('first call');
    expect(await mockFn()).toBe('success');
  });

  it('应该模拟实现', () => {
    const mockFn = jest.fn((a, b) => a + b);
    expect(mockFn(1, 2)).toBe(3);

    mockFn.mockImplementation((a, b) => a * b);
    expect(mockFn(2, 3)).toBe(6);
  });
});

模块模拟

// __mocks__/axios.js
export default {
  get: jest.fn(() => Promise.resolve({ data: {} })),
  post: jest.fn(() => Promise.resolve({ data: {} }))
};

// userService.test.js
import axios from 'axios';
import { getUser, createUser } from './userService';

jest.mock('axios');

describe('用户服务', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('应该获取用户', async () => {
    const mockUser = { id: 1, name: 'John' };
    axios.get.mockResolvedValue({ data: mockUser });

    const user = await getUser(1);

    expect(axios.get).toHaveBeenCalledWith('/users/1');
    expect(user).toEqual(mockUser);
  });

  it('应该创建用户', async () => {
    const newUser = { name: 'Jane' };
    const createdUser = { id: 2, name: 'Jane' };
    axios.post.mockResolvedValue({ data: createdUser });

    const user = await createUser(newUser);

    expect(axios.post).toHaveBeenCalledWith('/users', newUser);
    expect(user).toEqual(createdUser);
  });
});

部分模拟

// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;

// calculator.test.js
import * as utils from './utils';

jest.mock('./utils', () => ({
  ...jest.requireActual('./utils'),
  multiply: jest.fn()
}));

describe('计算器', () => {
  it('应该使用真实的add函数', () => {
    expect(utils.add(2, 3)).toBe(5);
  });

  it('应该使用模拟的multiply函数', () => {
    utils.multiply.mockReturnValue(10);
    expect(utils.multiply(2, 3)).toBe(10);
    expect(utils.multiply).toHaveBeenCalledWith(2, 3);
  });
});

间谍

监视方法

describe('间谍', () => {
  it('应该监视对象方法', () => {
    const calculator = {
      add: (a, b) => a + b
    };

    const spy = jest.spyOn(calculator, 'add');
    calculator.add(2, 3);

    expect(spy).toHaveBeenCalled();
    expect(spy).toHaveBeenCalledWith(2, 3);
    expect(spy).toHaveReturnedWith(5);

    spy.mockRestore();
  });

  it('应该监视并模拟实现', () => {
    const logger = {
      log: (message) => console.log(message)
    };

    const spy = jest.spyOn(logger, 'log').mockImplementation(() => {});
    logger.log('test');

    expect(spy).toHaveBeenCalledWith('test');
    spy.mockRestore();
  });

  it('应该监视getter', () => {
    const obj = {
      get value() {
        return 42;
      }
    };

    const spy = jest.spyOn(obj, 'value', 'get').mockReturnValue(100);
    expect(obj.value).toBe(100);
    spy.mockRestore();
  });
});

监视全局函数

describe('全局间谍', () => {
  it('应该监视console.log', () => {
    const spy = jest.spyOn(console, 'log').mockImplementation();
    console.log('test message');

    expect(spy).toHaveBeenCalledWith('test message');
    spy.mockRestore();
  });

  it('应该监视Date', () => {
    const mockDate = new Date('2024-01-01');
    const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);

    expect(new Date()).toBe(mockDate);
    spy.mockRestore();
  });
});

快照测试

基本快照

import { render } from '@testing-library/react';
import Button from './Button';

describe('按钮组件', () => {
  it('应该匹配快照', () => {
    const { container } = render(<Button label="Click me" />);
    expect(container.firstChild).toMatchSnapshot();
  });

  it('应该匹配内联快照', () => {
    const user = {
      name: 'John Doe',
      age: 30
    };
    expect(user).toMatchInlineSnapshot(`
      {
        "age": 30,
        "name": "John Doe",
      }
    `);
  });
});

属性匹配器

describe('带有动态数据的快照', () => {
  it('应该使用属性匹配器匹配快照', () => {
    const user = {
      id: generateId(),
      createdAt: new Date(),
      name: 'John Doe'
    };

    expect(user).toMatchSnapshot({
      id: expect.any(String),
      createdAt: expect.any(Date)
    });
  });
});

自定义序列化器

// custom-serializer.js
module.exports = {
  test(val) {
    return val && val.hasOwnProperty('_reactInternalFiber');
  },
  serialize(val, config, indentation, depth, refs, printer) {
    return `<ReactElement ${val.type} />`;
  }
};

// jest.config.js
module.exports = {
  snapshotSerializers: ['./custom-serializer.js']
};

测试模式

测试React组件

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Form from './Form';

describe('表单组件', () => {
  it('应该渲染表单字段', () => {
    render(<Form />);

    expect(screen.getByLabelText('Name')).toBeInTheDocument();
    expect(screen.getByLabelText('Email')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
  });

  it('应该处理用户输入', async () => {
    const user = userEvent.setup();
    render(<Form />);

    const nameInput = screen.getByLabelText('Name');
    await user.type(nameInput, 'John Doe');

    expect(nameInput).toHaveValue('John Doe');
  });

  it('应该提交表单', async () => {
    const onSubmit = jest.fn();
    render(<Form onSubmit={onSubmit} />);

    await userEvent.type(screen.getByLabelText('Name'), 'John Doe');
    await userEvent.type(screen.getByLabelText('Email'), 'john@example.com');
    await userEvent.click(screen.getByRole('button', { name: 'Submit' }));

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        name: 'John Doe',
        email: 'john@example.com'
      });
    });
  });

  it('应该显示验证错误', async () => {
    render(<Form />);

    const submitButton = screen.getByRole('button', { name: 'Submit' });
    await userEvent.click(submitButton);

    await waitFor(() => {
      expect(screen.getByText('Name is required')).toBeInTheDocument();
    });
  });
});

测试异步操作

describe('异步操作', () => {
  it('应该等待异步操作', async () => {
    const promise = fetchData();
    const data = await promise;
    expect(data).toBeDefined();
  });

  it('应该使用假定时器', () => {
    jest.useFakeTimers();

    const callback = jest.fn();
    setTimeout(callback, 1000);

    expect(callback).not.toHaveBeenCalled();
    jest.advanceTimersByTime(1000);
    expect(callback).toHaveBeenCalled();

    jest.useRealTimers();
  });

  it('应该运行所有定时器', () => {
    jest.useFakeTimers();

    const callback1 = jest.fn();
    const callback2 = jest.fn();

    setTimeout(callback1, 1000);
    setTimeout(callback2, 2000);

    jest.runAllTimers();

    expect(callback1).toHaveBeenCalled();
    expect(callback2).toHaveBeenCalled();

    jest.useRealTimers();
  });
});

测试错误边界

import { render, screen } from '@testing-library/react';
import ErrorBoundary from './ErrorBoundary';

const ThrowError = () => {
  throw new Error('Test error');
};

describe('错误边界', () => {
  it('应该捕获错误并显示后备', () => {
    // 为此测试抑制console.error
    const spy = jest.spyOn(console, 'error').mockImplementation();

    render(
      <ErrorBoundary>
        <ThrowError />
      </ErrorBoundary>
    );

    expect(screen.getByText('Something went wrong')).toBeInTheDocument();

    spy.mockRestore();
  });

  it('当没有错误时应渲染子元素', () => {
    render(
      <ErrorBoundary>
        <div>Content</div>
      </ErrorBoundary>
    );

    expect(screen.getByText('Content')).toBeInTheDocument();
  });
});

最佳实践

  1. 编写描述性测试名称 - 使用清晰、具体的名称来解释测试验证的内容
  2. 遵循AAA模式 - 使用Arrange、Act、Assert部分结构测试以提高清晰度
  3. 测试行为,而不是实现 - 关注代码做什么,而不是如何做
  4. 使用适当的匹配器 - 选择最具体的匹配器以获得更好的错误消息
  5. 模拟外部依赖 - 隔离单元测试与外部服务和模块
  6. 测试后清理 - 使用afterEach重置模拟和清理副作用
  7. 避免测试私有方法 - 测试公共接口并信任实现细节
  8. 有效使用设置和清理钩子 - 利用beforeEach/afterEach进行通用设置
  9. 测试边缘情况和错误条件 - 不要只测试快乐路径
  10. 保持测试快速和隔离 - 每个测试应独立运行且快速

常见陷阱

  1. 测试间未清理模拟 - 导致测试污染和不稳定测试
  2. 测试实现细节 - 使测试脆弱且难以维护
  3. 过度使用快照 - 快照应补充,而不是替代,特定断言
  4. 忘记等待异步操作 - 导致误报和不稳定测试
  5. 未使用适当匹配器 - 通用匹配器提供差的错误消息
  6. 模拟过多 - 过度模拟降低测试信心和价值
  7. 将集成测试写作单元测试 - 混合关注点使测试缓慢和复杂
  8. 未测试错误场景 - 只测试快乐路径会遗漏错误
  9. 测试间共享状态 - 全局变量和单例导致测试相互依赖
  10. 测试名称不清晰 - 模糊的名称使难以理解测试失败

何时使用此技能

  • 为函数和类编写单元测试
  • 测试具有用户交互的React组件
  • 模拟外部依赖和API
  • 为UI组件创建快照测试
  • 测试异步代码和promises
  • 实现间谍以验证函数调用
  • 测试错误处理和边缘情况
  • 设置测试夹具和测试数据
  • 调试失败测试和理解测试输出
  • 重构测试以提高可维护性