4、Mocha测试框架深入:describe与it块、钩子函数、异步测试与超时设置

Mocha 这个框架,说实话,是我用得最顺手的测试工具之一。它足够灵活,又不失强大。今天咱们就把它最核心的几个功能掰开揉碎了讲清楚。

4.1 describe 与 it:测试的组织单元

先聊聊最基础的结构。describe 和 it,说白了就是用来「分组」和「写用例」的。

describe 用来定义一个「测试套件」。你可以把它想象成一个文件夹,里面装着一堆相关的测试用例。它可以嵌套,用来表达更复杂的层级关系。

it 用来定义一个具体的「测试用例」。每个 it 块里只测一件事,这是原则。

describe('用户模块', function() {
  describe('登录功能', function() {
    it('应该能正确登录', function() {
      // 测试逻辑
    });

    it('密码错误时应该返回提示', function() {
      // 测试逻辑
    });
  });
});

我个人习惯,describe 的命名用名词短语,it 的命名用「应该...」这种句式。这样跑测试的时候,输出结果读起来就像一份需求文档,一目了然。

小技巧: 嵌套层级别超过三层,否则可读性会下降。我见过有人嵌套了五层,最后自己都找不到用例在哪。

4.2 钩子函数:before、after、beforeEach、afterEach

钩子函数是 Mocha 里非常实用的机制。它们会在测试执行的不同阶段自动运行。

钩子 执行时机 典型用途
before() 当前 describe 块内所有测试执行之前,只跑一次 初始化数据库连接、启动服务器
after() 当前 describe 块内所有测试执行之后,只跑一次 关闭数据库连接、清理全局资源
beforeEach() 当前 describe 块内每个 it 执行之前,都会跑一次 重置测试数据、创建新对象
afterEach() 当前 describe 块内每个 it 执行之后,都会跑一次 清理临时文件、记录测试日志

举个例子,假设我们要测试一个「用户注册」功能:

describe('用户注册', function() {
  let db;

  before(async function() {
    // 只做一次:连接数据库
    db = await connectToDatabase();
  });

  after(async function() {
    // 只做一次:断开连接
    await db.close();
  });

  beforeEach(async function() {
    // 每个用例前:清空用户表
    await db.collection('users').deleteMany({});
  });

  it('应该能注册新用户', async function() {
    const result = await registerUser({ name: '张三', email: 'test@test.com' });
    // 断言...
  });

  it('重复邮箱应该报错', async function() {
    await registerUser({ name: '李四', email: 'test@test.com' });
    // 再次注册相同邮箱,应该抛出异常
    // 断言...
  });
});

嗯,这里要注意一点:钩子函数的执行顺序是跟 describe 的嵌套层级有关的。外层钩子先执行,内层后执行。我曾经因为没搞清楚这个顺序,调试了一个下午才发现是钩子执行时机不对。

避坑指南: 不要在钩子函数里写太重的逻辑。比如在 beforeEach 里每次都重新创建数据库表,那测试跑起来会慢得让你怀疑人生。我曾经这么干过,结果一个测试跑了三分钟。

4.3 异步测试:三种写法

Node.js 里异步操作是家常便饭。Mocha 对异步测试的支持很到位,主要有三种方式。

4.3.1 回调风格

这是最传统的方式。在 it 的回调函数里加一个参数,通常叫 done。Mocha 会等到 done 被调用才结束这个用例。

it('应该能异步读取文件', function(done) {
  fs.readFile('/path/to/file', 'utf8', function(err, data) {
    if (err) {
      done(err); // 测试失败
      return;
    }
    // 断言...
    done(); // 测试通过
  });
});

注意,如果 done 被调用了多次,Mocha 会报错。所以记得加 return 或者用 if-else 控制好流程。

4.3.2 Promise 风格

如果你的函数返回 Promise,那就更简单了。直接 return 这个 Promise 就行。

it('应该能异步获取用户信息', function() {
  return fetchUser('123').then(function(user) {
    // 断言...
  });
});

Mocha 会等 Promise 状态变成 resolved 或 rejected。如果 rejected 了,测试就算失败。

4.3.3 async/await 风格

这是我最推荐的方式。代码写起来像同步一样,可读性最好。

it('应该能异步更新用户信息', async function() {
  const user = await fetchUser('123');
  const updatedUser = await updateUser('123', { name: '新名字' });
  // 断言...
});

说白了,async/await 就是 Promise 的语法糖。但用起来确实舒服很多。我个人现在写测试,90% 的情况都用 async/await。

我的经验: 如果你在 async 函数里忘了加 await,Mocha 不会报错,但测试可能会在异步操作完成前就结束了。结果就是测试永远通过,但测了个寂寞。所以写完后记得检查一下,每个异步调用前面有没有 await。

4.4 测试超时设置

默认情况下,Mocha 的测试超时时间是 2000 毫秒(2 秒)。如果某个测试用例执行时间超过了这个值,Mocha 就会认为它失败了。

但有些操作确实比较慢,比如文件上传、调用外部 API。这时候就需要调整超时时间。

有三种方式可以设置:

  1. 全局设置:在命令行加参数 --timeout 10000,所有用例的超时都变成 10 秒。
  2. 套件级别:在 describe 块里用 this.timeout(5000),影响当前套件下的所有用例。
  3. 用例级别:在 it 块里用 this.timeout(3000),只影响当前用例。
describe('文件上传', function() {
  // 这个套件下的所有用例,超时设为 10 秒
  this.timeout(10000);

  it('应该能上传大文件', function(done) {
    // 这个用例单独设置超时 30 秒
    this.timeout(30000);
    // 上传逻辑...
  });

  it('应该能上传小文件', function() {
    // 使用套件的 10 秒超时
    // 上传逻辑...
  });
});

这里有个坑:this.timeout() 在箭头函数里不能用。因为箭头函数不绑定自己的 this,它会继承外层作用域的 this。所以记得用普通函数。

避坑指南: 我曾经把一个超时时间设成了 0,以为这样就是不限制时间。结果 Mocha 直接报错,因为超时时间不能为 0。正确的做法是设成一个很大的值,比如 60000(一分钟)。但说实话,如果一个测试要跑一分钟以上,你可能得想想是不是设计有问题。

好了,Mocha 的这些核心功能就讲到这里。总结一下:describe 和 it 用来组织测试结构,钩子函数帮你管理测试生命周期,异步测试有三种写法任你选择,超时设置让你能灵活应对各种场景。把这些用熟了,写测试就不再是件痛苦的事了。