Jest高级测试技能Skill jest-advanced

此技能专注于Jest测试框架的高级功能应用,涵盖自定义匹配器、参数化测试、覆盖率配置和性能优化,适用于复杂场景和大规模测试套件的开发与维护。关键词:Jest, 测试, 自定义匹配器, 参数化测试, 覆盖率, 性能优化, 模拟策略, 断言, 契约测试。

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

name: jest-advanced user-invocable: false description: 使用高级Jest功能,包括自定义匹配器、使用test.each进行参数化测试、覆盖率配置和性能优化。 allowed-tools: [Read, Write, Edit, Bash, Glob, Grep]

Jest 高级

掌握高级Jest功能,包括自定义匹配器、使用test.each进行参数化测试、覆盖率配置和性能优化。此技能涵盖复杂场景和大型测试套件的复杂测试技术。

自定义匹配器

创建自定义匹配器

// matchers/toBeWithinRange.js
export function toBeWithinRange(received, floor, ceiling) {
  const pass = received >= floor && received <= ceiling;
  if (pass) {
    return {
      message: () =>
        `expected ${received} not to be within range ${floor} - ${ceiling}`,
      pass: true
    };
  } else {
    return {
      message: () =>
        `expected ${received} to be within range ${floor} - ${ceiling}`,
      pass: false
    };
  }
}

// jest.setup.js
import { toBeWithinRange } from './matchers/toBeWithinRange';

expect.extend({
  toBeWithinRange
});

// 测试文件
describe('自定义匹配器', () => {
  it('应检查数字是否在范围内', () => {
    expect(5).toBeWithinRange(1, 10);
    expect(15).not.toBeWithinRange(1, 10);
  });
});

异步自定义匹配器

// matchers/toResolveWithin.js
export async function toResolveWithin(received, timeout) {
  const startTime = Date.now();
  try {
    await received;
    const duration = Date.now() - startTime;
    const pass = duration <= timeout;

    return {
      message: () =>
        pass
          ? `expected promise not to resolve within ${timeout}ms (resolved in ${duration}ms)`
          : `expected promise to resolve within ${timeout}ms (took ${duration}ms)`,
      pass
    };
  } catch (error) {
    return {
      message: () => `expected promise to resolve but it rejected with ${error}`,
      pass: false
    };
  }
}

// 用法
expect.extend({ toResolveWithin });

it('应快速解析', async () => {
  await expect(fetchData()).toResolveWithin(1000);
});

类型安全自定义匹配器 (TypeScript)

// matchers/index.ts
interface CustomMatchers<R = unknown> {
  toBeWithinRange(floor: number, ceiling: number): R;
  toHaveValidEmail(): R;
}

declare global {
  namespace jest {
    interface Expect extends CustomMatchers {}
    interface Matchers<R> extends CustomMatchers<R> {}
    interface InverseAsymmetricMatchers extends CustomMatchers {}
  }
}

export function toBeWithinRange(
  this: jest.MatcherContext,
  received: number,
  floor: number,
  ceiling: number
): jest.CustomMatcherResult {
  const pass = received >= floor && received <= ceiling;
  return {
    message: () =>
      pass
        ? `expected ${received} not to be within range ${floor} - ${ceiling}`
        : `expected ${received} to be within range ${floor} - ${ceiling}`,
    pass
  };
}

export function toHaveValidEmail(
  this: jest.MatcherContext,
  received: string
): jest.CustomMatcherResult {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  const pass = emailRegex.test(received);
  return {
    message: () =>
      pass
        ? `expected ${received} not to be a valid email`
        : `expected ${received} to be a valid email`,
    pass
  };
}

// jest.setup.ts
import * as matchers from './matchers';
expect.extend(matchers);

参数化测试

使用数组的test.each

describe('加法', () => {
  test.each([
    [1, 1, 2],
    [1, 2, 3],
    [2, 1, 3],
  ])('add(%i, %i) 应等于 %i', (a, b, expected) => {
    expect(add(a, b)).toBe(expected);
  });
});

使用对象的test.each

describe('用户验证', () => {
  test.each([
    { email: 'test@example.com', valid: true },
    { email: 'invalid', valid: false },
    { email: 'test@', valid: false },
    { email: '@example.com', valid: false },
  ])('validateEmail($email) 应返回 $valid', ({ email, valid }) => {
    expect(validateEmail(email)).toBe(valid);
  });
});

使用模板字面量的test.each

describe('字符串操作', () => {
  test.each`
    input        | method      | expected
    ${'hello'}   | ${'upper'}  | ${'HELLO'}
    ${'WORLD'}   | ${'lower'}  | ${'world'}
    ${'HeLLo'}   | ${'title'}  | ${'Hello'}
  `('transform $input 使用 $method 应返回 $expected',
    ({ input, method, expected }) => {
      expect(transform(input, method)).toBe(expected);
    }
  );
});

describe.each用于多个测试套件

describe.each([
  { browser: 'Chrome', version: 90 },
  { browser: 'Firefox', version: 88 },
  { browser: 'Safari', version: 14 },
])('浏览器兼容性 - $browser', ({ browser, version }) => {
  it(`应支持 ${browser} 版本 ${version}`, () => {
    expect(isSupported(browser, version)).toBe(true);
  });

  it(`应处理 ${browser} 特定功能`, () => {
    expect(getFeatures(browser)).toBeDefined();
  });
});

覆盖率配置

高级覆盖率设置

// jest.config.js
module.exports = {
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html', 'json-summary'],

  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
    '!src/**/__tests__/**',
    '!src/**/types/**',
    '!src/index.{js,ts}',
  ],

  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    },
    './src/core/': {
      branches: 90,
      functions: 95,
      lines: 95,
      statements: 95
    },
    './src/utils/': {
      branches: 85,
      functions: 90,
      lines: 90,
      statements: 90
    }
  },

  coveragePathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
    '/build/',
    '/__mocks__/',
    '/coverage/'
  ]
};

自定义覆盖率报告器

// custom-reporter.js
class CustomCoverageReporter {
  constructor(globalConfig, options) {
    this._globalConfig = globalConfig;
    this._options = options;
  }

  onRunComplete(contexts, results) {
    const { coverageMap } = results;

    if (!coverageMap) {
      return;
    }

    const summary = coverageMap.getCoverageSummary();
    const metrics = {
      lines: summary.lines.pct,
      statements: summary.statements.pct,
      functions: summary.functions.pct,
      branches: summary.branches.pct
    };

    console.log('
覆盖率总结:');
    console.log(`行:      ${metrics.lines}%`);
    console.log(`语句: ${metrics.statements}%`);
    console.log(`函数:  ${metrics.functions}%`);
    console.log(`分支:   ${metrics.branches}%`);

    // 发送到外部服务
    if (this._options.webhook) {
      this.sendToWebhook(metrics, this._options.webhook);
    }
  }

  async sendToWebhook(metrics, url) {
    // 实现
  }
}

module.exports = CustomCoverageReporter;

// jest.config.js
module.exports = {
  coverageReporters: [
    'text',
    ['./custom-reporter.js', { webhook: 'https://example.com/coverage' }]
  ]
};

性能优化

并行运行测试

// jest.config.js
module.exports = {
  maxWorkers: '50%', // 使用可用CPU核心的50%
  // 或指定数字
  // maxWorkers: 4,

  // 在文件内并行化测试
  maxConcurrency: 5,

  // 缓存目录
  cacheDirectory: '.jest-cache',

  // 用于调试的串行运行测试
  // runInBand: false
};

选择性测试执行

// 只运行更改的测试
// package.json
{
  "scripts": {
    "test:changed": "jest --onlyChanged",
    "test:related": "jest --findRelatedTests src/modified-file.js"
  }
}

用于CI的测试分片

# 在多个CI机器上分割测试
jest --shard=1/3  # 运行前三分之一
jest --shard=2/3  # 运行中间三分之一
jest --shard=3/3  # 运行后三分之一

首次失败时停止

// jest.config.js
module.exports = {
  bail: 1, // 首次失败后停止
  // bail: true, // 任何失败后停止
};

高级模拟

具有不同返回值的模拟实现

describe('复杂模拟', () => {
  it('应在连续调用中返回不同值', () => {
    const mockFn = jest
      .fn()
      .mockReturnValueOnce(1)
      .mockReturnValueOnce(2)
      .mockReturnValue(3);

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

  it('应实现复杂逻辑', () => {
    const mockFn = jest.fn((x) => {
      if (x < 0) return 'negative';
      if (x === 0) return 'zero';
      return 'positive';
    });

    expect(mockFn(-5)).toBe('negative');
    expect(mockFn(0)).toBe('zero');
    expect(mockFn(5)).toBe('positive');
  });
});

模拟模块工厂

// dynamic-mock.test.js
jest.mock('./api', () => {
  const actual = jest.requireActual('./api');
  return {
    ...actual,
    fetchUser: jest.fn(),
    // 使用工厂获取动态值
    getDefaultUser: jest.fn(() => ({
      id: Math.random(),
      name: 'Test User'
    }))
  };
});

describe('动态模拟', () => {
  it('应生成不同的默认用户', () => {
    const user1 = getDefaultUser();
    const user2 = getDefaultUser();
    expect(user1.id).not.toBe(user2.id);
  });
});

模拟ES6类

// Database.js
export class Database {
  constructor(config) {
    this.config = config;
  }

  async connect() {
    // 实现
  }

  async query(sql) {
    // 实现
  }
}

// Database.test.js
import { Database } from './Database';

jest.mock('./Database');

describe('数据库模拟', () => {
  beforeEach(() => {
    Database.mockClear();
  });

  it('应模拟类构造函数', () => {
    const mockConnect = jest.fn();
    const mockQuery = jest.fn().mockResolvedValue([{ id: 1 }]);

    Database.mockImplementation(() => ({
      connect: mockConnect,
      query: mockQuery
    }));

    const db = new Database({ host: 'localhost' });
    expect(Database).toHaveBeenCalledWith({ host: 'localhost' });

    db.connect();
    expect(mockConnect).toHaveBeenCalled();
  });
});

高级断言

非对称匹配器

describe('非对称匹配器', () => {
  it('应匹配对象的一部分', () => {
    const user = {
      id: 1,
      name: 'John',
      email: 'john@example.com',
      createdAt: new Date()
    };

    expect(user).toEqual({
      id: expect.any(Number),
      name: 'John',
      email: expect.stringContaining('@'),
      createdAt: expect.any(Date)
    });
  });

  it('应匹配数组包含', () => {
    const arr = [1, 2, 3, 4, 5];
    expect(arr).toEqual(expect.arrayContaining([2, 4]));
  });

  it('应匹配对象包含', () => {
    const obj = { a: 1, b: 2, c: 3 };
    expect(obj).toEqual(expect.objectContaining({ a: 1, c: 3 }));
  });

  it('应使用自定义匹配器', () => {
    expect({ a: 1, b: 2 }).toEqual({
      a: expect.any(Number),
      b: expect.any(Number)
    });
  });
});

复杂断言

describe('复杂断言', () => {
  it('应验证复杂数据结构', () => {
    const response = {
      status: 200,
      data: {
        users: [
          { id: 1, name: 'John', roles: ['admin'] },
          { id: 2, name: 'Jane', roles: ['user'] }
        ],
        meta: {
          total: 2,
          page: 1
        }
      }
    };

    expect(response).toMatchObject({
      status: 200,
      data: {
        users: expect.arrayContaining([
          expect.objectContaining({
            name: 'John',
            roles: expect.arrayContaining(['admin'])
          })
        ]),
        meta: {
          total: expect.any(Number)
        }
      }
    });
  });
});

测试隔离和清理

重置模块注册表

describe('模块隔离', () => {
  beforeEach(() => {
    jest.resetModules();
  });

  it('应加载新模块实例', () => {
    const module1 = require('./counter');
    module1.increment();
    expect(module1.getCount()).toBe(1);

    jest.resetModules();

    const module2 = require('./counter');
    expect(module2.getCount()).toBe(0);
  });
});

清除、重置和恢复模拟

describe('模拟清理', () => {
  const mockFn = jest.fn();

  beforeEach(() => {
    mockFn.mockReturnValue(42);
  });

  afterEach(() => {
    // jest.clearAllMocks();    // 清除调用历史
    // jest.resetAllMocks();    // 清除调用历史和实现
    // jest.restoreAllMocks();  // 恢复原始实现
  });

  it('应理解模拟清理', () => {
    mockFn();
    expect(mockFn).toHaveBeenCalledTimes(1);

    // clearAllMocks: 移除调用历史但保留实现
    jest.clearAllMocks();
    expect(mockFn).toHaveBeenCalledTimes(0);
    expect(mockFn()).toBe(42); // 实现仍有效

    // resetAllMocks: 移除调用历史和实现
    jest.resetAllMocks();
    expect(mockFn()).toBeUndefined(); // 无实现

    // restoreAllMocks: 恢复原始实现(用于间谍)
    const obj = { method: () => 'original' };
    const spy = jest.spyOn(obj, 'method');
    spy.mockReturnValue('mocked');
    expect(obj.method()).toBe('mocked');
    jest.restoreAllMocks();
    expect(obj.method()).toBe('original');
  });
});

测试策略

契约测试

// 定义契约
const userContract = {
  id: expect.any(Number),
  name: expect.any(String),
  email: expect.stringMatching(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
  roles: expect.arrayContaining([expect.any(String)]),
  createdAt: expect.any(String)
};

describe('用户API契约', () => {
  it('应匹配用户契约', async () => {
    const user = await fetchUser(1);
    expect(user).toMatchObject(userContract);
  });

  it('应匹配用户列表契约', async () => {
    const users = await fetchUsers();
    expect(users).toEqual(
      expect.arrayContaining([
        expect.objectContaining(userContract)
      ])
    );
  });
});

数据驱动测试

const testCases = require('./test-data.json');

describe.each(testCases)('数据驱动测试', (testCase) => {
  it(`应处理 ${testCase.description}`, () => {
    const result = processData(testCase.input);
    expect(result).toEqual(testCase.expected);
  });
});

最佳实践

  1. 为领域特定断言使用自定义匹配器 - 为常见验证模式创建可重用匹配器
  2. 利用test.each进行参数化测试 - 减少重复并提高测试覆盖率
  3. 按目录配置覆盖率阈值 - 为关键代码路径设置更严格的要求
  4. 使用工作者优化测试执行 - 使用并行执行加快测试运行
  5. 实现适当的模拟清理 - 理解清除、重置和恢复之间的区别
  6. 使用非对称匹配器进行灵活断言 - 匹配部分对象和动态数据
  7. 为CI集成创建自定义报告器 - 将覆盖率数据发送到外部服务
  8. 使用模块重置隔离测试 - 防止共享模块状态导致的测试污染
  9. 使用bail获取快速反馈 - 在开发中首次失败时停止
  10. 实现契约测试 - 确保API响应匹配预期形状

常见陷阱

  1. 未正确清理模拟 - 使用错误的清理方法导致测试污染
  2. 过度参数化测试 - 过多test.each案例降低可读性
  3. 设置不切实际的覆盖率阈值 - 100%覆盖率要求减慢开发速度
  4. 未适当使用maxWorkers - 过多工作者会使CI系统不堪重负
  5. 忘记重置模块 - 测试之间的共享状态导致不稳定失败
  6. 创建过于复杂的自定义匹配器 - 保持匹配器简单和专注
  7. 在TypeScript中未键入自定义匹配器 - 缺少类型失去IDE支持
  8. 误用非对称匹配器 - 过于宽松的匹配器错过错误
  9. 忽略覆盖率报告 - 不根据覆盖率差距采取行动降低测试价值
  10. 每次更改都运行完整套件 - 使用–onlyChanged获取更快反馈

何时使用此技能

  • 为领域特定验证创建自定义匹配器
  • 实现具有许多测试用例的参数化测试
  • 为大型项目优化测试套件性能
  • 为CI/CD设置覆盖率要求
  • 实施高级模拟策略
  • 使用非对称匹配器测试复杂数据结构
  • 为外部集成创建自定义报告器
  • 调试测试隔离问题
  • 实施契约测试模式
  • 提高测试可维护性并减少重复