Mocha 基础测试 #

安装与配置 #

安装 Mocha #

bash
# 全局安装
npm install --global mocha

# 项目本地安装(推荐)
npm install --save-dev mocha

项目初始化 #

bash
# 创建项目
mkdir my-project
cd my-project
npm init -y

# 安装 Mocha 和断言库
npm install --save-dev mocha chai

# 创建测试目录
mkdir test

项目结构 #

text
my-project/
├── src/
│   └── math.js
├── test/
│   └── math.test.js
├── package.json
└── node_modules/

配置 package.json #

json
{
  "name": "my-project",
  "version": "1.0.0",
  "scripts": {
    "test": "mocha"
  },
  "devDependencies": {
    "chai": "^4.3.0",
    "mocha": "^10.0.0"
  }
}

测试的基本结构 #

describe 和 it #

Mocha 使用 describeit 组织测试:

javascript
// test/math.test.js
const { expect } = require('chai');
const { add, subtract } = require('../src/math');

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

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

    it('should add zero', function() {
      expect(add(5, 0)).to.equal(5);
    });
  });

  describe('subtract', function() {
    it('should subtract two numbers', function() {
      expect(subtract(5, 3)).to.equal(2);
    });
  });
});

第一个测试 #

创建被测试的模块:

javascript
// src/math.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('Division by zero');
  }
  return a / b;
}

module.exports = { add, subtract, multiply, divide };

创建测试文件:

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

describe('Math Module', function() {
  describe('add', function() {
    it('should return sum of two numbers', function() {
      expect(math.add(1, 2)).to.equal(3);
    });

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

    it('should handle zero', function() {
      expect(math.add(5, 0)).to.equal(5);
    });
  });

  describe('divide', function() {
    it('should divide two numbers', function() {
      expect(math.divide(6, 2)).to.equal(3);
    });

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

运行测试:

bash
npm test

# 输出
#   Math Module
#     add
#       ✓ should return sum of two numbers
#       ✓ should handle negative numbers
#       ✓ should handle zero
#     divide
#       ✓ should divide two numbers
#       ✓ should throw error when dividing by zero
#
#   5 passing (10ms)

测试接口风格 #

Mocha 支持多种测试接口风格:

BDD 风格(推荐) #

javascript
// BDD (Behavior Driven Development)
describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal([1, 2, 3].indexOf(4), -1);
    });

    it('should return the correct index when found', function() {
      assert.equal([1, 2, 3].indexOf(2), 1);
    });
  });
});

// 使用 context(describe 的别名)
context('when the array is empty', function() {
  it('should return -1', function() {
    assert.equal([].indexOf(1), -1);
  });
});

TDD 风格 #

javascript
// TDD (Test Driven Development)
suite('Array', function() {
  suite('#indexOf()', function() {
    test('should return -1 when not present', function() {
      assert.equal([1, 2, 3].indexOf(4), -1);
    });
  });
});

配置 TDD 风格:

bash
mocha --ui tdd test/

Exports 风格 #

javascript
// exports 风格
module.exports = {
  'Array': {
    '#indexOf()': {
      'should return -1 when not present': function() {
        assert.equal([1, 2, 3].indexOf(4), -1);
      }
    }
  }
};

QUnit 风格 #

javascript
// QUnit 风格
suite('Array');

test('should return -1 when not present', function() {
  assert.equal([1, 2, 3].indexOf(4), -1);
});

Require 风格 #

javascript
// require 风格
var suite = require('mocha').suite;
var test = require('mocha').test;

suite('Array', function() {
  test('should return -1 when not present', function() {
    assert.equal([1, 2, 3].indexOf(4), -1);
  });
});

组织测试 #

嵌套 describe #

javascript
describe('ShoppingCart', function() {
  describe('addItem', function() {
    describe('when item is available', function() {
      it('should add item to cart', function() {
        // ...
      });

      it('should update total price', function() {
        // ...
      });
    });

    describe('when item is out of stock', function() {
      it('should throw error', function() {
        // ...
      });
    });
  });

  describe('removeItem', function() {
    it('should remove item from cart', function() {
      // ...
    });
  });
});

使用 context #

contextdescribe 的别名,用于描述测试上下文:

javascript
describe('User Authentication', function() {
  context('when credentials are valid', function() {
    it('should return access token', function() {
      // ...
    });

    it('should set session cookie', function() {
      // ...
    });
  });

  context('when credentials are invalid', function() {
    it('should throw authentication error', function() {
      // ...
    });

    it('should not set session cookie', function() {
      // ...
    });
  });
});

测试命名规范 #

好的命名 #

javascript
// ✅ 描述行为
it('should return the sum of two numbers', function() {});
it('should throw error when input is invalid', function() {});
it('should return empty array when no items found', function() {});

// ✅ 使用中文描述
it('应该返回两个数的和', function() {});
it('输入无效时应该抛出错误', function() {});

// ✅ 描述预期结果
it('returns true when user is logged in', function() {});
it('returns false when password is incorrect', function() {});

不好的命名 #

javascript
// ❌ 太模糊
it('test1', function() {});
it('works', function() {});

// ❌ 技术细节而非行为
it('calls function', function() {});
it('returns value', function() {});

// ❌ 太长
it('should return the correct sum when adding two positive numbers together', function() {});

跳过测试 #

skip 方法 #

javascript
// 跳过单个测试
it.skip('this test is skipped', function() {
  expect(true).to.equal(false);
});

// 跳过整个 describe
describe.skip('Feature Not Ready', function() {
  it('should work', function() {
    // 这个测试不会运行
  });
});

// 使用 xit 和 xdescribe
xit('this is also skipped', function() {});
xdescribe('this suite is skipped', function() {});

条件跳过 #

javascript
const shouldRunSlowTests = process.env.RUN_SLOW_TESTS === 'true';

(shouldRunSlowTests ? it : it.skip)('slow test', function() {
  this.timeout(5000);
  // 耗时测试
});

// 根据环境跳过
const isCI = process.env.CI === 'true';

if (!isCI) {
  it('local only test', function() {
    // 只在本地运行
  });
}

仅运行特定测试 #

only 方法 #

javascript
// 只运行这一个测试
it.only('run this test only', function() {
  expect(1 + 1).to.equal(2);
});

// 只运行这个 describe
describe.only('Calculator', function() {
  it('should add', function() {
    // 这个会运行
  });
});

// 其他测试都会被跳过
it('this test is skipped', function() {
  // 不会运行
});

使用 fit 和 fdescribe #

javascript
// fit = it.only
fit('focused test', function() {});

// fdescribe = describe.only
fdescribe('focused suite', function() {});

测试超时 #

设置超时时间 #

javascript
// 设置整个套件超时
describe('slow operations', function() {
  this.timeout(5000); // 5秒

  it('should complete within 5 seconds', function(done) {
    setTimeout(done, 4000);
  });
});

// 设置单个测试超时
it('slow test', function(done) {
  this.timeout(3000);
  setTimeout(done, 2500);
});

// 使用 arrow function 注意事项
// ❌ 不能使用 arrow function 设置 this.timeout
describe('wrong', () => {
  it('test', () => {
    this.timeout(1000); // 错误!this 不是 Mocha 上下文
  });
});

// ✅ 使用普通函数
describe('correct', function() {
  it('test', function() {
    this.timeout(1000); // 正确
  });
});

不同级别的超时设置 #

javascript
// 全局超时(命令行)
// mocha --timeout 5000 test/

// 套件级别
describe('suite', function() {
  this.timeout(3000);

  // 测试级别
  it('test', function() {
    this.timeout(1000);
  });
});

// 钩子级别
describe('suite', function() {
  beforeEach(function() {
    this.timeout(2000);
  });
});

测试慢速警告 #

javascript
// 设置慢速阈值
describe('operations', function() {
  this.slow(100); // 超过 100ms 标记为慢

  it('fast test', function() {
    // < 50ms: 正常
  });

  it('medium test', function() {
    // 50-100ms: 黄色警告
  });

  it('slow test', function() {
    // > 100ms: 红色警告
  });
});

测试重试 #

对于不稳定的测试,可以设置重试次数:

javascript
// 整个套件重试
describe('flaky tests', function() {
  this.retries(3);

  it('unstable test', function() {
    // 最多重试 3 次
  });
});

// 单个测试重试
it('flaky test', function() {
  this.retries(2);
  // 最多重试 2 次
});

测试文件组织 #

文件命名约定 #

text
test/
├── unit/                    # 单元测试
│   ├── math.test.js
│   └── string.test.js
├── integration/             # 集成测试
│   ├── api.test.js
│   └── database.test.js
├── e2e/                     # 端到端测试
│   └── user-flow.test.js
└── helpers/                 # 测试辅助工具
    └── setup.js

测试文件结构 #

javascript
// test/user.test.js

// 1. 导入依赖
const { expect } = require('chai');
const User = require('../src/user');
const Database = require('../src/database');

// 2. 测试套件
describe('User', function() {
  // 3. 共享变量
  let user;
  let db;

  // 4. 生命周期钩子
  before(async function() {
    db = new Database('test');
    await db.connect();
  });

  after(async function() {
    await db.disconnect();
  });

  beforeEach(function() {
    user = new User('John', 'john@example.com');
  });

  // 5. 分组测试
  describe('constructor', function() {
    it('should create user with name and email', function() {
      expect(user.name).to.equal('John');
      expect(user.email).to.equal('john@example.com');
    });
  });

  describe('save', function() {
    it('should save user to database', async function() {
      const saved = await user.save(db);
      expect(saved.id).to.exist;
    });
  });
});

运行测试 #

命令行运行 #

bash
# 运行所有测试
mocha

# 运行指定文件
mocha test/math.test.js

# 运行指定目录
mocha test/unit/**/*.js

# 使用通配符
mocha "test/**/*.test.js"

# 递归运行
mocha --recursive test/

常用选项 #

bash
# 指定报告器
mocha --reporter spec

# 监听文件变化
mocha --watch

# 并行运行
mocha --parallel

# 指定超时时间
mocha --timeout 5000

# 显示慢测试
mocha --slow 100

# 增加详细输出
mocha --verbose

# 禁用颜色输出
mocha --no-colors

使用 npm scripts #

json
{
  "scripts": {
    "test": "mocha",
    "test:watch": "mocha --watch",
    "test:coverage": "nyc mocha",
    "test:unit": "mocha test/unit",
    "test:integration": "mocha test/integration"
  }
}

实用技巧 #

AAA 模式 #

Arrange(准备)- Act(执行)- Assert(断言):

javascript
it('should add item to cart', function() {
  // Arrange - 准备测试数据
  const cart = new Cart();
  const item = { id: 1, name: 'Product', price: 100 };

  // Act - 执行被测试的操作
  cart.addItem(item);

  // Assert - 验证结果
  expect(cart.items).to.have.lengthOf(1);
  expect(cart.items[0]).to.deep.equal(item);
  expect(cart.total).to.equal(100);
});

参数化测试 #

javascript
// 使用 forEach 实现参数化
const testCases = [
  { a: 1, b: 2, expected: 3 },
  { a: 2, b: 3, expected: 5 },
  { a: -1, b: -2, expected: -3 },
  { a: 0, b: 0, expected: 0 }
];

describe('add', function() {
  testCases.forEach(({ a, b, expected }) => {
    it(`should return ${expected} for ${a} + ${b}`, function() {
      expect(add(a, b)).to.equal(expected);
    });
  });
});

测试描述模板 #

javascript
describe('功能名称', function() {
  describe('方法名称', function() {
    context('场景描述', function() {
      it('应该 [预期行为]', function() {
        // 测试代码
      });
    });
  });
});

// 示例
describe('ShoppingCart', function() {
  describe('addItem', function() {
    context('当商品库存充足时', function() {
      it('应该成功添加商品到购物车', function() {
        // ...
      });
    });

    context('当商品库存不足时', function() {
      it('应该抛出库存不足错误', function() {
        // ...
      });
    });
  });
});

下一步 #

现在你已经掌握了 Mocha 基础测试的编写方法,接下来学习 断言匹配器 了解更多断言方式!

最后更新:2026-03-28