Jest 基础:describe 和 it/test 块、expect 和匹配器、setup 和 teardown 函数

好,咱们进入 Jest 的核心地带。说实话,Jest 的 API 并不多,但每个都挺讲究。我刚开始用 Jest 时,总觉得它跟别的测试框架差不多,用着用着才发现——嗯,细节里藏着不少好东西。

describe 和 it/test 块:测试的组织方式

先说说 describe。它就是个分组工具,把相关的测试用例归到一起。我个人习惯这样用:

describe('计算器模块', () => {
  describe('加法功能', () => {
    it('应该正确计算两个正数相加', () => {
      expect(add(1, 2)).toBe(3);
    });

    it('应该正确处理负数', () => {
      expect(add(-1, -2)).toBe(-3);
    });
  });

  describe('减法功能', () => {
    it('应该正确计算两个正数相减', () => {
      expect(subtract(5, 3)).toBe(2);
    });
  });
});

你想想看,describe 可以嵌套。外层描述模块,内层描述具体功能。这样跑测试时,控制台输出的信息特别清晰。

至于 ittest,它们完全等价。我更喜欢用 it,因为读起来更像自然语言——「it should do something」。当然,你用 test 也没问题,纯粹是个人偏好。

小技巧: 我建议每个 it 块只测一个行为。别贪多,一个 it 里塞五六个断言,出了问题你都不知道是哪个挂了。

expect 和匹配器:断言的艺术

expect 是 Jest 的断言入口。它返回一个「期望对象」,然后你调用各种「匹配器」来做判断。说白了,就是「我期望这个值怎么样」。

常用的匹配器我列一下:

匹配器 用途 示例
toBe() 严格相等(===) expect(1 + 1).toBe(2)
toEqual() 深度相等(对象/数组) expect({a: 1}).toEqual({a: 1})
toBeTruthy() 判断是否为真值 expect('hello').toBeTruthy()
toBeFalsy() 判断是否为假值 expect(0).toBeFalsy()
toContain() 数组/字符串是否包含 expect([1,2,3]).toContain(2)
toThrow() 是否抛出异常 expect(() => fn()).toThrow()

这里有个坑,我踩过。用 toBe 比较对象:

// 这样会失败!
expect({ a: 1 }).toBe({ a: 1 });

// 应该用 toEqual
expect({ a: 1 }).toEqual({ a: 1 });

为什么会这样?因为 toBe 用的是 ===,两个对象引用不同。而 toEqual 会递归比较每个字段。记住这个区别,能省不少调试时间。

注意: 测试浮点数时别用 toBe。0.1 + 0.2 不等于 0.3,这是 JavaScript 的精度问题。用 toBeCloseTo() 代替。

setup 和 teardown 函数:测试的准备工作

写测试时,经常需要做一些「前置工作」和「收尾工作」。Jest 提供了四个钩子函数:

  • beforeEach:每个测试用例执行前运行
  • afterEach:每个测试用例执行后运行
  • beforeAll:所有测试用例执行前运行一次
  • afterAll:所有测试用例执行后运行一次

看个实际例子:

describe('用户管理', () => {
  let userDb;

  beforeAll(() => {
    // 只做一次,比如连接数据库
    userDb = new Database();
  });

  beforeEach(() => {
    // 每个测试前清空数据,保证隔离
    userDb.clear();
  });

  afterEach(() => {
    // 每个测试后记录日志
    console.log('测试完成');
  });

  afterAll(() => {
    // 关闭数据库连接
    userDb.close();
  });

  it('应该能创建用户', () => {
    userDb.create({ name: '张三' });
    expect(userDb.count()).toBe(1);
  });

  it('应该能删除用户', () => {
    userDb.create({ name: '李四' });
    userDb.delete('李四');
    expect(userDb.count()).toBe(0);
  });
});

我在项目中遇到过一个问题:测试之间互相影响。后来发现是 beforeEach 里忘了重置某个全局状态。从那以后,我养成了习惯——每个测试用例都假设「世界是干净的」。

核心原则: 测试用例之间必须完全隔离。一个测试的失败不应该影响其他测试。setup 和 teardown 就是帮你做到这一点的。

作用域:钩子函数的执行顺序

这里有个细节,很多人会搞混。钩子函数的作用域跟 describe 块有关:

describe('外层', () => {
  beforeEach(() => console.log('外层 beforeEach'));

  describe('内层', () => {
    beforeEach(() => console.log('内层 beforeEach'));

    it('测试用例', () => {
      console.log('执行测试');
    });
  });
});

// 输出顺序:
// 外层 beforeEach
// 内层 beforeEach
// 执行测试

说白了,Jest 会从外到内执行所有 beforeEach,然后执行测试,再按从内到外的顺序执行 afterEach。这个机制很实用——外层做通用准备,内层做特定准备。

建议: 别把太多逻辑塞进钩子函数。我曾经见过一个 beforeEach 里写了 50 行代码,调试起来特别痛苦。保持钩子函数简洁,复杂的准备工作可以抽成单独的函数。

嗯,以上就是 Jest 测试的基础骨架。掌握了 describeitexpect 和钩子函数,你就能写出结构清晰、隔离良好的测试用例了。下一章咱们会深入异步测试,那才是真正考验功底的地方。