Mocha
GitHub概要
mochajs/mocha
☕️ simple, flexible, fun javascript test framework for node.js & the browser
トピックス
スター履歴
テストツール
Mocha
概要
MochaはJavaScriptのための柔軟で成熟したテストフレームワークです。2012年から開発が続けられており、Node.jsとブラウザの両方で動作します。BDD(Behavior Driven Development)、TDD(Test Driven Development)、Exports、QUnit、Requireなど複数のインターフェースをサポートし、開発者の好みに応じてテストスタイルを選択できます。豊富なプラグインエコシステムと高い拡張性により、あらゆるプロジェクトに適応できる汎用性の高いテストツールです。
詳細
Mochaの最大の特徴は、その柔軟性と拡張性にあります。アサーションライブラリは含まれておらず、Chai、Should.js、Node.jsの組み込みassertモジュールなど、好みのライブラリを自由に選択できます。この設計により、プロジェクトの要件に応じて最適な組み合わせを構築できます。
非同期テストについては、Promise、async/await、コールバックのすべてをサポートし、複雑な非同期処理も簡潔に記述できます。フックシステム(before、after、beforeEach、afterEach)により、テストの前後で必要な処理を実行できます。また、スイートとテストの動的生成、条件付きスキップ、リトライ機能なども提供しています。
レポート機能では、デフォルトで20以上のレポーターが用意されており、JSON、HTML、TAP、JUnit XMLなど様々な形式で結果を出力できます。CI/CDパイプラインとの統合も容易です。
メリット・デメリット
メリット
- 高い柔軟性: アサーションライブラリやレポーターを自由に選択可能
- 豊富なプラグイン: 長い歴史による充実したエコシステム
- 複数インターフェース: BDD、TDD、Exportsなど多様なテストスタイル
- 非同期サポート: Promise、async/await、コールバックすべてに対応
- ブラウザ対応: Node.jsとブラウザの両方で動作
- 安定性: 長期間にわたる実績と信頼性
デメリット
- 設定の複雑さ: 柔軟性の代償として初期設定が煩雑
- アサーション別途必要: 標準でアサーションライブラリが含まれない
- 学習コスト: 多様なオプションや機能の理解が必要
- パフォーマンス: 新しいフレームワークと比べて実行速度が劣る場合がある
参考ページ
書き方の例
Hello World
// package.json
{
"scripts": {
"test": "mocha"
},
"devDependencies": {
"mocha": "^10.0.0",
"chai": "^4.3.0"
}
}
// test/hello.test.js
const assert = require('assert')
describe('Hello World', function() {
it('should return greeting message', function() {
function greet(name) {
return `Hello, ${name}!`
}
assert.strictEqual(greet('Mocha'), 'Hello, Mocha!')
})
})
基本テスト
const { expect } = require('chai')
describe('Calculator', function() {
let calculator
beforeEach(function() {
calculator = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => {
if (b === 0) throw new Error('Division by zero')
return a / b
}
}
})
describe('#add()', function() {
it('should add positive numbers', function() {
expect(calculator.add(2, 3)).to.equal(5)
})
it('should handle negative numbers', function() {
expect(calculator.add(-1, 1)).to.equal(0)
})
it('should handle floating point precision', function() {
expect(calculator.add(0.1, 0.2)).to.be.closeTo(0.3, 0.001)
})
})
describe('#divide()', function() {
it('should divide numbers correctly', function() {
expect(calculator.divide(10, 2)).to.equal(5)
})
it('should throw error for division by zero', function() {
expect(() => calculator.divide(10, 0)).to.throw('Division by zero')
})
})
})
モック
const sinon = require('sinon')
const { expect } = require('chai')
describe('API Client', function() {
let fetchStub
beforeEach(function() {
fetchStub = sinon.stub(global, 'fetch')
})
afterEach(function() {
fetchStub.restore()
})
it('should make API calls correctly', async function() {
const mockResponse = {
json: sinon.stub().resolves({ data: 'test data' }),
ok: true
}
fetchStub.resolves(mockResponse)
const apiClient = {
async getData() {
const response = await fetch('/api/data')
return response.json()
}
}
const result = await apiClient.getData()
expect(fetchStub).to.have.been.calledWith('/api/data')
expect(result).to.deep.equal({ data: 'test data' })
})
it('should handle API errors', async function() {
fetchStub.rejects(new Error('Network error'))
const apiClient = {
async getData() {
const response = await fetch('/api/data')
return response.json()
}
}
try {
await apiClient.getData()
expect.fail('Should have thrown an error')
} catch (error) {
expect(error.message).to.equal('Network error')
}
})
})
非同期テスト
const { expect } = require('chai')
describe('Async Operations', function() {
// Promiseを使用
it('should handle promises', function() {
const asyncFunction = () => {
return new Promise((resolve) => {
setTimeout(() => resolve('async result'), 100)
})
}
return asyncFunction().then(result => {
expect(result).to.equal('async result')
})
})
// async/awaitを使用
it('should handle async/await', async function() {
const asyncFunction = () => {
return new Promise((resolve) => {
setTimeout(() => resolve('async result'), 100)
})
}
const result = await asyncFunction()
expect(result).to.equal('async result')
})
// コールバックを使用
it('should handle callbacks', function(done) {
const asyncFunction = (callback) => {
setTimeout(() => callback(null, 'callback result'), 100)
}
asyncFunction((error, result) => {
try {
expect(error).to.be.null
expect(result).to.equal('callback result')
done()
} catch (err) {
done(err)
}
})
})
// タイムアウトの設定
it('should handle slow operations', async function() {
this.timeout(5000) // 5秒のタイムアウト
const slowFunction = () => {
return new Promise(resolve => {
setTimeout(() => resolve('slow result'), 2000)
})
}
const result = await slowFunction()
expect(result).to.equal('slow result')
})
})
セットアップ・ティアダウン
const { expect } = require('chai')
describe('Database Operations', function() {
let database
// スイート全体の前後
before(async function() {
console.log('Setting up test database...')
database = await setupTestDatabase()
})
after(async function() {
console.log('Cleaning up test database...')
await cleanupTestDatabase(database)
})
// 各テストの前後
beforeEach(async function() {
await database.clear()
await database.seed([
{ id: 1, name: 'Test User 1' },
{ id: 2, name: 'Test User 2' }
])
})
afterEach(async function() {
await database.clear()
})
describe('User Operations', function() {
it('should create a new user', async function() {
const user = await database.createUser({ name: 'New User' })
expect(user.id).to.exist
expect(user.name).to.equal('New User')
})
it('should find existing users', async function() {
const users = await database.findUsers()
expect(users).to.have.length(2)
expect(users[0].name).to.equal('Test User 1')
})
it('should update user information', async function() {
const updatedUser = await database.updateUser(1, { name: 'Updated User' })
expect(updatedUser.name).to.equal('Updated User')
})
})
})
高度な機能
const { expect } = require('chai')
describe('Advanced Features', function() {
// 条件付きテスト
it('should run only on Node.js', function() {
if (typeof window !== 'undefined') {
this.skip()
}
expect(process.version).to.match(/^v\d+/)
})
// 動的テスト生成
const testCases = [
{ input: 'hello', expected: 'HELLO' },
{ input: 'world', expected: 'WORLD' },
{ input: '123', expected: '123' }
]
testCases.forEach(({ input, expected }) => {
it(`should convert "${input}" to uppercase`, function() {
expect(input.toUpperCase()).to.equal(expected)
})
})
// リトライ機能
it('should retry failed tests', function() {
this.retries(3) // 最大3回リトライ
// フレーキーなテストのシミュレーション
if (Math.random() > 0.7) {
expect(true).to.be.true
} else {
expect(false).to.be.true
}
})
// カスタムレポーター使用
it('should support custom reporting', function() {
console.log('Custom test output')
expect(true).to.be.true
})
// グローバルフック(root-level)
before(function() {
console.log('Global setup')
})
// ネストしたdescribe
describe('Nested Suite', function() {
describe('Deep Nesting', function() {
it('should support deep nesting', function() {
expect('nested').to.equal('nested')
})
})
})
})