Mocha

テストユニットテストJavaScriptNode.js歴史ある柔軟

GitHub概要

mochajs/mocha

☕️ simple, flexible, fun javascript test framework for node.js & the browser

ホームページ:https://mochajs.org
スター22,803
ウォッチ394
フォーク3,033
作成日:2011年3月7日
言語:JavaScript
ライセンス:MIT License

トピックス

bddbrowserjavascriptmochamochajsnodenodejstddtesttest-frameworktestingtesting-tools

スター履歴

mochajs/mocha Star History
データ取得日時: 2025/7/20 02:18

テストツール

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')
      })
    })
  })
})