Mocha 高级特性 #

浏览器测试 #

设置浏览器环境 #

Mocha 可以直接在浏览器中运行测试:

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Mocha Tests</title>
  <link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css">
</head>
<body>
  <div id="mocha"></div>

  <!-- 加载 Mocha -->
  <script src="https://unpkg.com/mocha/mocha.js"></script>

  <!-- 加载断言库 -->
  <script src="https://unpkg.com/chai/chai.js"></script>

  <!-- 配置 Mocha -->
  <script>
    mocha.setup('bdd');
    var expect = chai.expect;
  </script>

  <!-- 加载被测试代码 -->
  <script src="src/math.js"></script>

  <!-- 加载测试文件 -->
  <script src="test/math.test.js"></script>

  <!-- 运行测试 -->
  <script>
    mocha.run();
  </script>
</body>
</html>

浏览器测试示例 #

javascript
// src/math.js
var MathUtils = {
  add: function(a, b) {
    return a + b;
  },

  subtract: function(a, b) {
    return a - b;
  },

  multiply: function(a, b) {
    return a * b;
  },

  divide: function(a, b) {
    if (b === 0) {
      throw new Error('Division by zero');
    }
    return a / b;
  }
};

// test/math.test.js
describe('MathUtils', function() {
  describe('add', function() {
    it('should add two numbers', function() {
      expect(MathUtils.add(1, 2)).to.equal(3);
    });

    it('should handle negative numbers', function() {
      expect(MathUtils.add(-1, -2)).to.equal(-3);
    });
  });

  describe('divide', function() {
    it('should throw error when dividing by zero', function() {
      expect(function() {
        MathUtils.divide(1, 0);
      }).to.throw('Division by zero');
    });
  });
});

使用 ES 模块 #

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Mocha ES Module Tests</title>
  <link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css">
</head>
<body>
  <div id="mocha"></div>

  <script type="module">
    import { mocha } from 'https://unpkg.com/mocha/mocha.js';
    import { expect } from 'https://unpkg.com/chai/chai.js';
    import { add, subtract } from './src/math.module.js';

    mocha.setup('bdd');

    describe('Math', function() {
      it('should add', function() {
        expect(add(1, 2)).to.equal(3);
      });
    });

    mocha.run();
  </script>
</body>
</html>

延迟根套件 #

使用 --delay #

延迟根套件允许在运行测试前执行异步操作:

bash
mocha --delay
javascript
// test/delayed.test.js
setTimeout(function() {
  // 异步初始化完成后再运行测试
  describe('Delayed Tests', function() {
    it('should run after delay', function() {
      expect(true).to.be.true;
    });
  });

  // 必须调用 run() 开始测试
  run();
}, 1000);

实际应用 #

javascript
// test/delayed.test.js
const Database = require('../src/database');

let db;

setTimeout(async function() {
  try {
    // 异步初始化数据库
    db = await Database.connect('test-db');

    describe('Database Tests', function() {
      before(function() {
        // db 已经初始化
      });

      it('should query users', async function() {
        const users = await db.query('SELECT * FROM users');
        expect(users).to.be.an('array');
      });
    });

    run();
  } catch (err) {
    console.error('Setup failed:', err);
    process.exit(1);
  }
}, 0);

全局变量 #

定义全局变量 #

javascript
// .mocharc.js
module.exports = {
  global: ['myGlobal', 'anotherGlobal']
};
javascript
// test/setup.js
global.myGlobal = {
  config: {
    apiUrl: 'https://api.example.com'
  }
};

global.anotherGlobal = function() {
  return 'hello';
};

使用全局变量 #

javascript
// test/api.test.js
describe('API', function() {
  it('should use global config', function() {
    const url = myGlobal.config.apiUrl;
    expect(url).to.equal('https://api.example.com');
  });

  it('should call global function', function() {
    expect(anotherGlobal()).to.equal('hello');
  });
});

自定义接口 #

创建自定义接口 #

javascript
// custom-interface.js
module.exports = function(suite) {
  suite.on('pre-require', function(context, file, mocha) {
    // 自定义 test 函数
    context.testCase = function(title, fn) {
      context.it(title, fn);
    };

    // 自定义 describe 函数
    context.feature = function(title, fn) {
      context.describe(title, fn);
    };

    // 自定义断言
    context.assert = function(actual, expected, message) {
      if (actual !== expected) {
        throw new Error(message || `Expected ${expected} but got ${actual}`);
      }
    };
  });
};

使用自定义接口 #

bash
mocha --ui ./custom-interface.js test/
javascript
// test/custom.test.js
feature('User Management', function() {
  testCase('should create user', function() {
    assert(1 + 1, 2, 'Math is broken');
  });
});

测试调试 #

使用 Node.js 调试器 #

bash
# 启用调试模式
mocha --inspect test/

# 等待调试器连接
mocha --inspect-brk test/

# 使用 Chrome DevTools
# chrome://inspect

使用 VS Code 调试 #

json
// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Mocha Tests",
      "program": "${workspaceFolder}/node_modules/mocha/bin/mocha",
      "args": [
        "--timeout", "999999",
        "--colors",
        "${workspaceFolder}/test/**/*.test.js"
      ],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen"
    }
  ]
}

使用 console.log 调试 #

javascript
describe('Debug', function() {
  it('should debug', function() {
    const data = { name: 'John', age: 30 };

    console.log('Data:', data);
    console.log('JSON:', JSON.stringify(data, null, 2));

    expect(data.name).to.equal('John');
  });
});

使用 --verbose 输出 #

bash
mocha --verbose test/

测试隔离 #

使用 --file 选项 #

javascript
// .mocharc.js
module.exports = {
  file: [
    './test/setup.js',
    './test/helpers.js'
  ]
};
javascript
// test/setup.js
// 在所有测试之前执行
process.env.NODE_ENV = 'test';

before(function() {
  console.log('Global setup');
});

after(function() {
  console.log('Global teardown');
});

测试隔离最佳实践 #

javascript
describe('Isolation', function() {
  let sandbox;

  beforeEach(function() {
    // 创建 Sinon sandbox
    sandbox = sinon.createSandbox();
  });

  afterEach(function() {
    // 恢复所有 stub 和 spy
    sandbox.restore();
  });

  it('should be isolated', function() {
    const stub = sandbox.stub(obj, 'method');
    // 测试代码
  });

  it('should not affect other tests', function() {
    // obj.method 已恢复
  });
});

动态生成测试 #

使用循环生成测试 #

javascript
describe('Dynamic Tests', function() {
  const testCases = [
    { input: 1, expected: 2 },
    { input: 2, expected: 4 },
    { input: 3, expected: 6 },
    { input: 10, expected: 20 }
  ];

  testCases.forEach(({ input, expected }) => {
    it(`should double ${input} to ${expected}`, function() {
      expect(double(input)).to.equal(expected);
    });
  });
});

使用 describe.each #

javascript
// 自定义 describe.each 实现
function describeEach(title, testCases, fn) {
  describe(title, function() {
    testCases.forEach((testCase, index) => {
      const caseTitle = typeof testCase === 'object'
        ? JSON.stringify(testCase)
        : String(testCase);

      it(`case ${index + 1}: ${caseTitle}`, function() {
        fn(testCase);
      });
    });
  });
}

// 使用
describeEach('Math operations', [
  { a: 1, b: 2, sum: 3 },
  { a: 5, b: 3, sum: 8 },
  { a: -1, b: 1, sum: 0 }
], function({ a, b, sum }) {
  expect(add(a, b)).to.equal(sum);
});

测试性能优化 #

并行测试 #

javascript
// .mocharc.js
module.exports = {
  parallel: true,
  jobs: 4
};

增量测试 #

bash
# 只运行修改的测试
mocha --watch

# 使用 git diff 确定测试文件
git diff --name-only HEAD~1 | grep test | xargs mocha

测试分组 #

json
// package.json
{
  "scripts": {
    "test:unit": "mocha test/unit --timeout 2000",
    "test:integration": "mocha test/integration --timeout 10000",
    "test:e2e": "mocha test/e2e --timeout 30000"
  }
}

测试报告增强 #

多报告器 #

javascript
// mocha-multi-reporters
// npm install mocha-multi-reporters --save-dev

// .mocharc.js
module.exports = {
  reporter: 'mocha-multi-reporters',
  reporterOptions: {
    reporterEnabled: 'spec, xunit',
    xunitReporterOptions: {
      output: 'test-results.xml'
    }
  }
};

测试结果输出 #

javascript
// test/hooks.js
afterEach(function() {
  if (this.currentTest.state === 'failed') {
    console.log('\nFailed test:', this.currentTest.fullTitle());
    console.log('Error:', this.currentTest.err.message);
    console.log('Stack:', this.currentTest.err.stack);
  }
});

测试数据管理 #

使用 Fixtures #

javascript
// test/fixtures/users.js
module.exports = {
  validUser: {
    name: 'John Doe',
    email: 'john@example.com',
    age: 30
  },

  invalidUser: {
    name: '',
    email: 'invalid-email',
    age: -1
  },

  adminUser: {
    name: 'Admin',
    email: 'admin@example.com',
    role: 'admin'
  }
};
javascript
// test/user.test.js
const fixtures = require('./fixtures/users');

describe('User', function() {
  it('should create valid user', function() {
    const user = new User(fixtures.validUser);
    expect(user.isValid()).to.be.true;
  });

  it('should reject invalid user', function() {
    const user = new User(fixtures.invalidUser);
    expect(user.isValid()).to.be.false;
  });
});

使用 Factory #

javascript
// test/factories/user.js
class UserFactory {
  static create(overrides = {}) {
    return {
      id: Math.random().toString(36).substr(2, 9),
      name: 'Test User',
      email: 'test@example.com',
      createdAt: new Date(),
      ...overrides
    };
  }

  static createMany(count, overrides = {}) {
    return Array.from({ length: count }, (_, i) =>
      this.create({ ...overrides, name: `User ${i + 1}` })
    );
  }
}

module.exports = UserFactory;
javascript
// test/user.test.js
const UserFactory = require('./factories/user');

describe('User', function() {
  it('should create user', function() {
    const userData = UserFactory.create({ name: 'John' });
    expect(userData.name).to.equal('John');
  });

  it('should create many users', function() {
    const users = UserFactory.createMany(5);
    expect(users).to.have.lengthOf(5);
  });
});

测试最佳实践 #

1. 测试命名 #

javascript
// ✅ 好的命名
describe('UserService', function() {
  describe('createUser', function() {
    context('when input is valid', function() {
      it('should create a new user with correct data', function() {
        // ...
      });
    });

    context('when email is invalid', function() {
      it('should throw ValidationError', function() {
        // ...
      });
    });
  });
});

// ❌ 不好的命名
describe('test', function() {
  it('test1', function() {});
});

2. 测试结构 #

javascript
describe('Feature', function() {
  // 1. 设置和清理
  let subject;
  let dependency;

  beforeEach(function() {
    dependency = sinon.stub();
    subject = new Feature(dependency);
  });

  afterEach(function() {
    sinon.restore();
  });

  // 2. 测试分组
  describe('method', function() {
    context('scenario', function() {
      it('should behave', function() {
        // Arrange
        dependency.returns('mocked');

        // Act
        const result = subject.method();

        // Assert
        expect(result).to.equal('expected');
      });
    });
  });
});

3. 测试覆盖 #

javascript
describe('Calculator', function() {
  describe('divide', function() {
    // 正常情况
    it('should divide two numbers', function() {
      expect(divide(6, 2)).to.equal(3);
    });

    // 边界情况
    it('should handle division by 1', function() {
      expect(divide(5, 1)).to.equal(5);
    });

    it('should handle negative numbers', function() {
      expect(divide(-6, 2)).to.equal(-3);
    });

    it('should handle decimal results', function() {
      expect(divide(5, 2)).to.equal(2.5);
    });

    // 错误情况
    it('should throw error when dividing by zero', function() {
      expect(() => divide(1, 0)).to.throw('Division by zero');
    });
  });
});

4. 测试隔离 #

javascript
// ✅ 好的做法:每个测试独立
describe('Counter', function() {
  let counter;

  beforeEach(function() {
    counter = new Counter(0);
  });

  it('should increment', function() {
    counter.increment();
    expect(counter.value).to.equal(1);
  });

  it('should start from 0', function() {
    expect(counter.value).to.equal(0);
  });
});

// ❌ 不好的做法:测试之间有依赖
describe('Counter', function() {
  let counter = new Counter(0);

  it('should increment', function() {
    counter.increment();
    expect(counter.value).to.equal(1);
  });

  it('depends on previous test', function() {
    // 这个测试依赖上一个测试
    expect(counter.value).to.equal(1);
  });
});

常见问题解决 #

1. 测试超时 #

javascript
// 增加超时时间
describe('Slow Tests', function() {
  this.timeout(10000);

  it('should complete', function(done) {
    slowOperation(done);
  });
});

2. 未处理的 Promise 拒绝 #

javascript
// 确保所有 Promise 都被处理
it('should handle promise', function() {
  return promiseOperation()
    .then(result => {
      expect(result).to.exist;
    });
});

// 或使用 async/await
it('should handle promise', async function() {
  const result = await promiseOperation();
  expect(result).to.exist;
});

3. 内存泄漏 #

javascript
describe('Memory Management', function() {
  let interval;

  afterEach(function() {
    // 清理定时器
    if (interval) {
      clearInterval(interval);
      interval = null;
    }
  });

  it('should not leak memory', function() {
    interval = setInterval(() => {}, 1000);
    // 测试代码
  });
});

总结 #

恭喜你完成了 Mocha 测试框架的学习!你已经掌握了:

  1. 基础测试:describe、it、测试组织
  2. 断言库:Chai 的 expect、should、assert 风格
  3. 生命周期钩子:before、after、beforeEach、afterEach
  4. 异步测试:回调、Promise、async/await
  5. Mock 和 Stub:使用 Sinon 模拟依赖
  6. 配置优化:配置文件、报告器、并行测试
  7. 高级特性:浏览器测试、调试、最佳实践

继续实践,编写高质量的测试代码!

最后更新:2026-03-28