Mocha 教程

1. 核心知识点讲解

1.1 Mocha 简介

Mocha 是一个功能强大的 JavaScript 测试框架,专注于异步测试。它提供了灵活的测试结构和丰富的钩子函数,支持多种断言库和测试风格。

1.2 安装和配置

安装 Mocha 和断言库:

npm install --save-dev mocha chai

配置 package.json:

{
  "scripts": {
    "test": "mocha"
  }
}

创建 mocha.opts 配置文件(Mocha v6 及以下):

--recursive
--reporter spec
--timeout 5000

使用命令行参数(Mocha v7+):

mocha --recursive --reporter spec --timeout 5000

1.3 基本测试结构

测试文件命名约定:

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

基本测试结构:

const assert = require('assert');

describe('测试套件', function() {
  it('测试用例', function() {
    assert.equal(1 + 1, 2);
  });
});

// 使用箭头函数
describe('测试套件', () => {
  it('测试用例', () => {
    assert.equal(1 + 1, 2);
  });
});

1.4 测试生命周期

测试钩子:

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

  after(function() {
    // 在所有测试结束后运行
  });

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

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

  it('测试用例 1', function() {
    // 测试代码
  });

  it('测试用例 2', function() {
    // 测试代码
  });
});

1.5 异步测试

回调风格:

describe('异步测试', function() {
  it('使用回调', function(done) {
    setTimeout(function() {
      assert.equal(1 + 1, 2);
      done(); // 必须调用 done() 表示测试完成
    }, 1000);
  });
});

Promise 风格:

describe('异步测试', function() {
  it('使用 Promise', function() {
    return new Promise(function(resolve) {
      setTimeout(function() {
        assert.equal(1 + 1, 2);
        resolve();
      }, 1000);
    });
  });
});

async/await 风格:

describe('异步测试', function() {
  it('使用 async/await', async function() {
    await new Promise(resolve => setTimeout(resolve, 1000));
    assert.equal(1 + 1, 2);
  });
});

1.6 断言库

内置断言:

const assert = require('assert');
assert.equal(1, 1);
assert.strictEqual(1, '1'); // 失败
assert.deepEqual({ a: 1 }, { a: 1 });
assert.throws(() => { throw new Error('错误'); });

Chai 断言库:

const { expect, assert, should } = require('chai');

// expect 风格
expect(1 + 1).to.equal(2);
expect({ a: 1 }).to.deep.equal({ a: 1 });
expect(true).to.be.true;

// assert 风格
assert.equal(1 + 1, 2);
assert.deepEqual({ a: 1 }, { a: 1 });

// should 风格
should();
(1 + 1).should.equal(2);
({ a: 1 }).should.deep.equal({ a: 1 });

1.7 测试覆盖率

安装 istanbul/nyc:

npm install --save-dev nyc

配置 package.json:

{
  "scripts": {
    "test": "mocha",
    "coverage": "nyc mocha"
  }
}

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

npm run coverage

1.8 测试报告

内置报告器:

  • spec - 默认,详细的测试结果
  • dot - 点矩阵形式
  • nyan - 彩虹猫形式
  • json - JSON 格式
  • html - HTML 格式

使用自定义报告器:

mocha --reporter nyan

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
};

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

describe('计算器测试', function() {
  describe('加法测试', function() {
    it('1 + 1 应该等于 2', function() {
      expect(add(1, 2)).to.equal(3);
    });

    it('-1 + 1 应该等于 0', function() {
      expect(add(-1, 1)).to.equal(0);
    });

    it('0 + 0 应该等于 0', function() {
      expect(add(0, 0)).to.equal(0);
    });
  });

  describe('减法测试', function() {
    it('5 - 3 应该等于 2', function() {
      expect(subtract(5, 3)).to.equal(2);
    });

    it('1 - 5 应该等于 -4', function() {
      expect(subtract(1, 5)).to.equal(-4);
    });
  });

  describe('乘法测试', function() {
    it('2 * 3 应该等于 6', function() {
      expect(multiply(2, 3)).to.equal(6);
    });

    it('-1 * 5 应该等于 -5', function() {
      expect(multiply(-1, 5)).to.equal(-5);
    });
  });

  describe('除法测试', function() {
    it('6 / 2 应该等于 3', function() {
      expect(divide(6, 2)).to.equal(3);
    });

    it('5 / 2 应该等于 2.5', function() {
      expect(divide(5, 2)).to.equal(2.5);
    });

    it('除数为零应该抛出错误', function() {
      expect(() => divide(1, 0)).to.throw('除数不能为零');
    });
  });
});

2.2 异步测试案例

场景: 测试一个异步函数

实现:

// src/userService.js
function getUserById(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: `User ${id}` });
      } else {
        reject(new Error('无效的用户 ID'));
      }
    }, 1000);
  });
}

module.exports = { getUserById };

// test/userService.test.js
const { expect } = require('chai');
const { getUserById } = require('../src/userService');

describe('用户服务测试', function() {
  this.timeout(2000); // 增加超时时间

  it('获取有效用户 ID 应该返回用户信息', async function() {
    const user = await getUserById(1);
    expect(user).to.deep.equal({ id: 1, name: 'User 1' });
  });

  it('获取无效用户 ID 应该抛出错误', async function() {
    try {
      await getUserById(-1);
      // 如果没有抛出错误,测试失败
      expect.fail('应该抛出错误');
    } catch (error) {
      expect(error.message).to.equal('无效的用户 ID');
    }
  });

  it('使用 Promise 测试异步函数', function() {
    return getUserById(2)
      .then(user => {
        expect(user).to.deep.equal({ id: 2, name: 'User 2' });
      });
  });

  it('使用回调测试异步函数', function(done) {
    getUserById(3)
      .then(user => {
        expect(user).to.deep.equal({ id: 3, name: 'User 3' });
        done();
      })
      .catch(done);
  });
});

2.3 测试钩子案例

场景: 测试一个需要初始化和清理的组件

实现:

// src/database.js
class Database {
  constructor() {
    this.connection = null;
  }

  connect() {
    this.connection = { id: Math.random() };
    return this.connection;
  }

  disconnect() {
    this.connection = null;
  }

  query(sql) {
    if (!this.connection) {
      throw new Error('未连接数据库');
    }
    return `执行 SQL: ${sql}`;
  }
}

module.exports = Database;

// test/database.test.js
const { expect } = require('chai');
const Database = require('../src/database');

describe('数据库测试', function() {
  let db;

  beforeEach(function() {
    // 每个测试前创建新的数据库实例
    db = new Database();
  });

  afterEach(function() {
    // 每个测试后清理
    if (db.connection) {
      db.disconnect();
    }
  });

  it('连接数据库应该返回连接对象', function() {
    const connection = db.connect();
    expect(connection).to.be.an('object');
    expect(db.connection).to.equal(connection);
  });

  it('未连接数据库执行查询应该抛出错误', function() {
    expect(() => db.query('SELECT * FROM users')).to.throw('未连接数据库');
  });

  it('连接数据库后执行查询应该成功', function() {
    db.connect();
    const result = db.query('SELECT * FROM users');
    expect(result).to.equal('执行 SQL: SELECT * FROM users');
  });

  it('断开连接后连接应该为 null', function() {
    db.connect();
    db.disconnect();
    expect(db.connection).to.be.null;
  });
});

3. 代码示例

3.1 完整的测试项目结构

项目结构:

project/
├── src/
│   ├── calculator.js
│   ├── userService.js
│   └── database.js
├── test/
│   ├── calculator.test.js
│   ├── userService.test.js
│   └── database.test.js
├── package.json
└── .nycrc

3.2 运行测试

运行所有测试:

npm test

运行特定测试文件:

mocha test/calculator.test.js

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

npm run coverage

3.3 测试配置示例

package.json:

{
  "name": "mocha-test-demo",
  "version": "1.0.0",
  "description": "Mocha 测试示例",
  "main": "index.js",
  "scripts": {
    "test": "mocha --recursive --reporter spec --timeout 5000",
    "coverage": "nyc mocha --recursive --reporter spec --timeout 5000"
  },
  "devDependencies": {
    "chai": "^4.3.4",
    "mocha": "^9.1.3",
    "nyc": "^15.1.0"
  }
}

.nycrc:

{
  "exclude": [
    "test/**",
    "node_modules/**"
  ],
  "reporter": [
    "text-summary",
    "html"
  ],
  "check-coverage": true,
  "branches": 80,
  "functions": 80,
  "lines": 80,
  "statements": 80
}

3.4 高级测试技巧

测试重试:

describe('重试测试', function() {
  it('应该重试失败的测试', function() {
    // 随机失败的测试
    if (Math.random() > 0.5) {
      throw new Error('随机失败');
    }
  }).retries(3); // 最多重试 3 次
});

测试跳过:

describe('跳过测试', function() {
  it.skip('应该跳过这个测试', function() {
    // 这个测试不会运行
  });

  it('应该运行这个测试', function() {
    // 这个测试会运行
  });
});

条件测试:

describe('条件测试', function() {
  (process.env.NODE_ENV === 'test' ? it : it.skip)('只在测试环境运行', function() {
    // 这个测试只在测试环境运行
  });
});

测试速度标记:

describe('测试速度', function() {
  it('快速测试', function() {
    // 快速运行的测试
  });

  it('慢速测试', function() {
    // 慢速运行的测试
  }).slow(1000); // 标记为慢速测试
});

4. 总结

Mocha 是一个功能强大的 JavaScript 测试框架,提供了灵活的测试结构和丰富的钩子函数。本教程介绍了 Mocha 的核心概念,包括:

  • 安装和配置
  • 基本测试结构
  • 测试生命周期和钩子函数
  • 异步测试支持
  • 断言库集成
  • 测试覆盖率报告
  • 测试报告器

通过学习这些概念,你可以开始在项目中使用 Mocha 编写测试,提高代码质量和可靠性。Mocha 的灵活性使其适合各种类型的测试场景,从简单的单元测试到复杂的集成测试。

5. 进一步学习资源

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

« 上一篇 Jest 教程 下一篇 » Chai 教程