Aller au contenu

Feuille de chaleur Mocha

Mocha - Essais JavaScript simples, flexibles et amusants

Mocha est un cadre de test JavaScript riche en fonctionnalités fonctionnant sur Node.js et dans le navigateur, rendant les tests asynchrones simples et amusants. Les tests Mocha fonctionnent en série, ce qui permet un rapport souple et précis, tout en mappant des exceptions non reconnues aux bons cas de test.

Copier toutes les commandes Générer PDF

Sommaire

  • [Installation] (#installation)
  • [Pour commencer] (#getting-started)
  • [Structure d'essai] (#test-structure)
  • [Hooks] (#hooks)
  • [Assertions] (#assertions)
  • [Essais d'Async] (#async-testing)
  • [Configuration] (#configuration)
  • [Reporters] (#reporters)
  • [Essais de navigation] (#browser-testing)
  • [Frappe] (#mocking)
  • [Caractéristiques avancées] (#advanced-features)
  • [Plugins] (#plugins)
  • [CI/CD Intégration] (#cicd-integration)
  • [Meilleures pratiques] (#best-practices)
  • [Débogage] (#debugging)
  • [Performance] (#performance)
  • [Dépannage] (#troubleshooting)

Installation

Installation de base

# Install Mocha globally
npm install -g mocha

# Install Mocha locally (recommended)
npm install --save-dev mocha

# Install with assertion library
npm install --save-dev mocha chai

# Install with additional tools
npm install --save-dev mocha chai sinon nyc

Configuration du projet

# Initialize new project
mkdir my-mocha-project
cd my-mocha-project
npm init -y

# Install dependencies
npm install --save-dev mocha chai

# Create test directory
mkdir test

# Create first test file
touch test/test.js
```_

### Je vous en prie. Configuration
```json
{
  "scripts": {
    "test": "mocha",
    "test:watch": "mocha --watch",
    "test:coverage": "nyc mocha",
    "test:reporter": "mocha --reporter spec",
    "test:grep": "mocha --grep 'pattern'",
    "test:timeout": "mocha --timeout 5000"
  },
  "devDependencies": {
    "mocha": "^10.0.0",
    "chai": "^4.3.0",
    "sinon": "^15.0.0",
    "nyc": "^15.1.0"
  }
}
```_

### Structure du répertoire
my-project/ ├── lib/ │ ├── calculator.js │ └── utils.js ├── test/ │ ├── calculator.test.js │ ├── utils.test.js │ └── helpers/ │ └── setup.js ├── .mocharc.json ├── package.json └── README.md
## Commencer

### Premier essai
```javascript
// lib/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('Division by zero');
  }
  return a / b;
}

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

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

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

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

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

  describe('Division', function() {
    it('should divide positive numbers', function() {
      expect(divide(10, 2)).to.equal(5);
    });

    it('should throw error for division by zero', function() {
      expect(() => divide(10, 0)).to.throw('Division by zero');
    });
  });
});

Essais de course

# Run all tests
npm test

# Run specific test file
npx mocha test/calculator.test.js

# Run tests with pattern
npx mocha test/**/*.test.js

# Run tests with grep
npx mocha --grep "Addition"

# Run tests in watch mode
npx mocha --watch

# Run tests with reporter
npx mocha --reporter json

Syntaxe d'essai de base

// Test suite
describe('Feature Name', function() {
  // Test case
  it('should do something', function() {
    // Test implementation
  });

  // Pending test
  it('should do something else');

  // Skipped test
  it.skip('should skip this test', function() {
    // This won't run
  });

  // Only run this test
  it.only('should only run this test', function() {
    // Only this test will run
  });
});

Structure d'essai

Décrire les blocs

describe('User Management', function() {
  describe('User Creation', function() {
    it('should create a new user', function() {
      // Test implementation
    });

    it('should validate user data', function() {
      // Test implementation
    });
  });

  describe('User Authentication', function() {
    it('should authenticate valid user', function() {
      // Test implementation
    });

    it('should reject invalid credentials', function() {
      // Test implementation
    });
  });
});

Décrit les nids

describe('API', function() {
  describe('Users Endpoint', function() {
    describe('GET /users', function() {
      it('should return all users', function() {
        // Test implementation
      });

      it('should return 200 status', function() {
        // Test implementation
      });
    });

    describe('POST /users', function() {
      it('should create new user', function() {
        // Test implementation
      });

      it('should return 201 status', function() {
        // Test implementation
      });
    });
  });
});

Organisation des essais

// Good: Descriptive test names
describe('Email Validator', function() {
  it('should return true for valid email addresses', function() {
    // Test implementation
  });

  it('should return false for invalid email addresses', function() {
    // Test implementation
  });

  it('should handle edge cases like empty strings', function() {
    // Test implementation
  });
});

// Good: Group related functionality
describe('Shopping Cart', function() {
  describe('Adding Items', function() {
    it('should add item to cart');
    it('should update cart total');
    it('should increment item count');
  });

  describe('Removing Items', function() {
    it('should remove item from cart');
    it('should update cart total');
    it('should decrement item count');
  });
});

Contexte des essais

describe('User Service', function() {
  context('when user exists', function() {
    it('should return user data', function() {
      // Test implementation
    });

    it('should update user successfully', function() {
      // Test implementation
    });
  });

  context('when user does not exist', function() {
    it('should return null', function() {
      // Test implementation
    });

    it('should throw error on update', function() {
      // Test implementation
    });
  });
});

Crochets

Crochets de base

describe('Database Tests', function() {
  // Runs once before all tests in this describe block
  before(function() {
    console.log('Setting up database connection');
    // Setup code here
  });

  // Runs once after all tests in this describe block
  after(function() {
    console.log('Closing database connection');
    // Cleanup code here
  });

  // Runs before each test in this describe block
  beforeEach(function() {
    console.log('Preparing test data');
    // Setup for each test
  });

  // Runs after each test in this describe block
  afterEach(function() {
    console.log('Cleaning up test data');
    // Cleanup after each test
  });

  it('should save user', function() {
    // Test implementation
  });

  it('should find user', function() {
    // Test implementation
  });
});

Crochets Async

describe('Async Setup', function() {
  before(async function() {
    this.timeout(5000); // Increase timeout for setup

    // Async setup
    this.database = await connectToDatabase();
    this.server = await startServer();
  });

  after(async function() {
    // Async cleanup
    await this.database.close();
    await this.server.stop();
  });

  beforeEach(async function() {
    // Clear database before each test
    await this.database.clear();

    // Seed test data
    await this.database.seed({
      users: [
        { name: 'John', email: 'john@example.com' },
        { name: 'Jane', email: 'jane@example.com' }
      ]
    });
  });

  it('should find users', async function() {
    const users = await this.database.findAll('users');
    expect(users).to.have.length(2);
  });
});

Héritage de crochet

describe('Parent Suite', function() {
  before(function() {
    console.log('Parent before');
  });

  beforeEach(function() {
    console.log('Parent beforeEach');
  });

  describe('Child Suite', function() {
    before(function() {
      console.log('Child before');
    });

    beforeEach(function() {
      console.log('Child beforeEach');
    });

    it('should inherit hooks', function() {
      // Execution order:
      // 1. Parent before
      // 2. Child before
      // 3. Parent beforeEach
      // 4. Child beforeEach
      // 5. Test execution
    });
  });
});

Crochets conditionnés

describe('Conditional Setup', function() {
  before(function() {
    if (process.env.NODE_ENV === 'test') {
      // Only run in test environment
      this.mockServer = startMockServer();
    }
  });

  beforeEach(function() {
    // Skip setup for specific tests
    if (this.currentTest.title.includes('integration')) {
      this.skip();
    }
  });

  it('should run unit test', function() {
    // This will run
  });

  it('should run integration test', function() {
    // This will be skipped by beforeEach
  });
});

Hypothèses

Assertions de Chai

const { expect } = require('chai');

describe('Chai Assertions', function() {
  it('should test equality', function() {
    expect(2 + 2).to.equal(4);
    expect({ name: 'John' }).to.deep.equal({ name: 'John' });
    expect([1, 2, 3]).to.deep.equal([1, 2, 3]);
  });

  it('should test truthiness', function() {
    expect(true).to.be.true;
    expect(false).to.be.false;
    expect(null).to.be.null;
    expect(undefined).to.be.undefined;
    expect('hello').to.exist;
  });

  it('should test types', function() {
    expect('hello').to.be.a('string');
    expect(42).to.be.a('number');
    expect([]).to.be.an('array');
    expect({}).to.be.an('object');
    expect(() => {}).to.be.a('function');
  });

  it('should test properties', function() {
    const obj = { name: 'John', age: 30 };
    expect(obj).to.have.property('name');
    expect(obj).to.have.property('age', 30);
    expect(obj).to.have.all.keys('name', 'age');
  });

  it('should test arrays', function() {
    const arr = [1, 2, 3, 4, 5];
    expect(arr).to.have.length(5);
    expect(arr).to.include(3);
    expect(arr).to.include.members([2, 4]);
    expect(arr).to.have.ordered.members([1, 2, 3, 4, 5]);
  });

  it('should test strings', function() {
    expect('hello world').to.contain('world');
    expect('hello world').to.match(/^hello/);
    expect('hello world').to.have.length(11);
  });
});

Assertions personnalisées

const { expect } = require('chai');

// Extend Chai with custom assertion
chai.use(function(chai, utils) {
  chai.Assertion.addMethod('validEmail', function() {
    const obj = this._obj;
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

    this.assert(
      emailRegex.test(obj),
      'expected #{this} to be a valid email',
      'expected #{this} not to be a valid email'
    );
  });
});

describe('Custom Assertions', function() {
  it('should validate email addresses', function() {
    expect('user@example.com').to.be.validEmail;
    expect('invalid-email').to.not.be.validEmail;
  });
});

Doit Style Assertions

const should = require('chai').should();

describe('Should Style', function() {
  it('should use should syntax', function() {
    const user = { name: 'John', age: 30 };

    user.should.be.an('object');
    user.should.have.property('name', 'John');
    user.age.should.equal(30);

    const numbers = [1, 2, 3];
    numbers.should.have.length(3);
    numbers.should.include(2);
  });
});

Assister les Assertions de Style

const assert = require('chai').assert;

describe('Assert Style', function() {
  it('should use assert syntax', function() {
    const user = { name: 'John', age: 30 };

    assert.isObject(user);
    assert.property(user, 'name');
    assert.equal(user.name, 'John');
    assert.equal(user.age, 30);

    const numbers = [1, 2, 3];
    assert.lengthOf(numbers, 3);
    assert.include(numbers, 2);
  });
});

Essais d'Async

Les promesses

describe('Promise Testing', function() {
  it('should resolve promise', function() {
    return fetchUser(1).then(user => {
      expect(user.name).to.equal('John');
    });
  });

  it('should reject promise', function() {
    return fetchUser(-1).catch(error => {
      expect(error.message).to.equal('User not found');
    });
  });

  it('should use async/await', async function() {
    const user = await fetchUser(1);
    expect(user.name).to.equal('John');
  });

  it('should handle async errors', async function() {
    try {
      await fetchUser(-1);
      throw new Error('Should have thrown');
    } catch (error) {
      expect(error.message).to.equal('User not found');
    }
  });
});

Rappels

describe('Callback Testing', function() {
  it('should handle callbacks with done', function(done) {
    fetchUserCallback(1, (error, user) => {
      if (error) return done(error);

      try {
        expect(user.name).to.equal('John');
        done();
      } catch (assertionError) {
        done(assertionError);
      }
    });
  });

  it('should handle callback errors', function(done) {
    fetchUserCallback(-1, (error, user) => {
      try {
        expect(error).to.exist;
        expect(error.message).to.equal('User not found');
        expect(user).to.not.exist;
        done();
      } catch (assertionError) {
        done(assertionError);
      }
    });
  });
});

Délais

describe('Timeout Testing', function() {
  // Set timeout for entire suite
  this.timeout(5000);

  it('should complete within timeout', function(done) {
    // Set timeout for specific test
    this.timeout(2000);

    setTimeout(() => {
      expect(true).to.be.true;
      done();
    }, 1000);
  });

  it('should handle slow operations', async function() {
    this.timeout(10000);

    const result = await slowOperation();
    expect(result).to.exist;
  });

  it('should disable timeout', function(done) {
    this.timeout(0); // Disable timeout

    // Very slow operation
    setTimeout(done, 30000);
  });
});

Remboursements

describe('Retry Testing', function() {
  // Retry failed tests
  this.retries(3);

  it('should retry flaky test', function() {
    // This test might fail randomly
    if (Math.random() < 0.7) {
      throw new Error('Random failure');
    }
    expect(true).to.be.true;
  });

  it('should retry specific test', function() {
    this.retries(5);

    // Test implementation
  });
});

Configuration

Fichier de configuration de Mocha

// .mocharc.json
{
  "spec": "test/**/*.test.js",
  "require": ["test/helpers/setup.js"],
  "timeout": 5000,
  "reporter": "spec",
  "recursive": true,
  "exit": true,
  "bail": false,
  "grep": "",
  "invert": false,
  "checkLeaks": true,
  "globals": ["expect", "sinon"],
  "retries": 0,
  "slow": 75,
  "ui": "bdd"
}

JavaScript Configuration

// .mocharc.js
module.exports = {
  spec: 'test/**/*.test.js',
  require: ['test/helpers/setup.js'],
  timeout: 5000,
  reporter: 'spec',
  recursive: true,
  exit: true,

  // Environment-specific configuration
  ...(process.env.NODE_ENV === 'ci' && {
    reporter: 'json',
    timeout: 10000,
    retries: 2
  })
};

Configuration du fichier

// test/helpers/setup.js
const chai = require('chai');
const sinon = require('sinon');

// Global setup
global.expect = chai.expect;
global.sinon = sinon;

// Chai plugins
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));

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

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

beforeEach(function() {
  // Reset sinon stubs/spies
  sinon.restore();
});

Variables d'environnement

// test/helpers/setup.js
process.env.NODE_ENV = 'test';
process.env.DATABASE_URL = 'mongodb://localhost:27017/test';
process.env.API_URL = 'http://localhost:3001';

// Load environment-specific config
if (process.env.CI) {
  // CI-specific setup
  process.env.TIMEOUT = '10000';
} else {
  // Local development setup
  process.env.TIMEOUT = '5000';
}

Configurations multiples

// mocha.config.js
const baseConfig = {
  timeout: 5000,
  recursive: true,
  exit: true
};

const configs = {
  unit: {
    ...baseConfig,
    spec: 'test/unit/**/*.test.js',
    reporter: 'spec'
  },

  integration: {
    ...baseConfig,
    spec: 'test/integration/**/*.test.js',
    timeout: 10000,
    reporter: 'json'
  },

  e2e: {
    ...baseConfig,
    spec: 'test/e2e/**/*.test.js',
    timeout: 30000,
    reporter: 'tap'
  }
};

module.exports = configs[process.env.TEST_TYPE] || configs.unit;

Reporters

Reporters intégrés

# Spec reporter (default)
npx mocha --reporter spec

# JSON reporter
npx mocha --reporter json

# TAP reporter
npx mocha --reporter tap

# Dot reporter
npx mocha --reporter dot

# Progress reporter
npx mocha --reporter progress

# Min reporter
npx mocha --reporter min

# Landing reporter
npx mocha --reporter landing

# List reporter
npx mocha --reporter list

Rapporteur personnalisé

// reporters/custom-reporter.js
function CustomReporter(runner) {
  const stats = runner.stats;

  runner.on('start', function() {
    console.log('🚀 Starting test run...');
  });

  runner.on('suite', function(suite) {
    if (suite.root) return;
    console.log(`📁 ${suite.title}`);
  });

  runner.on('test', function(test) {
    console.log(`  ▶️  ${test.title}`);
  });

  runner.on('pass', function(test) {
    console.log(`  ✅ ${test.title} (${test.duration}ms)`);
  });

  runner.on('fail', function(test, err) {
    console.log(`  ❌ ${test.title}`);
    console.log(`     ${err.message}`);
  });

  runner.on('end', function() {
    console.log(`🏁 ${stats.passes} passed, ${stats.failures} failed`);
  });
}

module.exports = CustomReporter;

// Usage
// npx mocha --reporter ./reporters/custom-reporter.js

Reporter avec sortie de fichier

// reporters/file-reporter.js
const fs = require('fs');
const path = require('path');

function FileReporter(runner, options) {
  const reportPath = options.reporterOptions?.output || 'test-results.json';
  const results = {
    stats: {},
    tests: [],
    failures: []
  };

  runner.on('start', function() {
    results.stats.start = new Date();
  });

  runner.on('pass', function(test) {
    results.tests.push({
      title: test.title,
      fullTitle: test.fullTitle(),
      duration: test.duration,
      state: 'passed'
    });
  });

  runner.on('fail', function(test, err) {
    results.tests.push({
      title: test.title,
      fullTitle: test.fullTitle(),
      duration: test.duration,
      state: 'failed',
      error: err.message
    });

    results.failures.push({
      title: test.fullTitle(),
      error: err.message,
      stack: err.stack
    });
  });

  runner.on('end', function() {
    results.stats.end = new Date();
    results.stats.duration = results.stats.end - results.stats.start;
    results.stats.passes = runner.stats.passes;
    results.stats.failures = runner.stats.failures;

    fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
    console.log(`Report written to ${reportPath}`);
  });
}

module.exports = FileReporter;

Reporters multiples

# Install multi-reporter
npm install --save-dev mocha-multi-reporters

# Configuration file
# multi-reporter-config.json
{
  "reporterEnabled": "spec,json,tap",
  "jsonReporterOptions": {
    "output": "test-results.json"
  },
  "tapReporterOptions": {
    "output": "test-results.tap"
  }
}

# Run with multiple reporters
npx mocha --reporter mocha-multi-reporters --reporter-options configFile=multi-reporter-config.json

Essai du navigateur

Configuration du navigateur

<!-- test/browser/index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Mocha Browser 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>

  <!-- Chai -->
  <script src="https://unpkg.com/chai/chai.js"></script>

  <!-- Setup -->
  <script>
    mocha.setup('bdd');
    const expect = chai.expect;
  </script>

  <!-- Your code -->
  <script src="../lib/calculator.js"></script>

  <!-- Your tests -->
  <script src="calculator.test.js"></script>

  <!-- Run tests -->
  <script>
    mocha.run();
  </script>
</body>
</html>

Fichier de test du navigateur

// test/browser/calculator.test.js
describe('Calculator (Browser)', function() {
  it('should add numbers', function() {
    expect(add(2, 3)).to.equal(5);
  });

  it('should work with DOM', function() {
    const div = document.createElement('div');
    div.textContent = 'Hello World';
    document.body.appendChild(div);

    expect(div.textContent).to.equal('Hello World');

    document.body.removeChild(div);
  });

  it('should test localStorage', function() {
    localStorage.setItem('test', 'value');
    expect(localStorage.getItem('test')).to.equal('value');
    localStorage.removeItem('test');
  });
});

Intégration du paquet Web

// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './test/browser/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'test-bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
};

Intégration des chiots

// test/browser/puppeteer.test.js
const puppeteer = require('puppeteer');
const { expect } = require('chai');

describe('Browser Tests with Puppeteer', function() {
  let browser, page;

  before(async function() {
    browser = await puppeteer.launch();
    page = await browser.newPage();
  });

  after(async function() {
    await browser.close();
  });

  it('should run tests in browser', async function() {
    await page.goto('file://' + __dirname + '/index.html');

    // Wait for tests to complete
    await page.waitForFunction(() => {
      return window.mochaResults && window.mochaResults.complete;
    });

    const results = await page.evaluate(() => window.mochaResults);
    expect(results.failures).to.equal(0);
  });
});

Fumer

Intégration des Sinons

const sinon = require('sinon');
const { expect } = require('chai');

describe('Mocking with Sinon', function() {
  afterEach(function() {
    sinon.restore();
  });

  it('should mock functions', function() {
    const callback = sinon.fake();
    const proxy = sinon.fake.returns(42);

    callback('hello', 'world');
    expect(callback.calledWith('hello', 'world')).to.be.true;

    expect(proxy()).to.equal(42);
  });

  it('should stub methods', function() {
    const user = {
      getName: () => 'John',
      setName: (name) => { this.name = name; }
    };

    const stub = sinon.stub(user, 'getName').returns('Jane');

    expect(user.getName()).to.equal('Jane');
    expect(stub.calledOnce).to.be.true;
  });

  it('should spy on methods', function() {
    const user = {
      getName: () => 'John'
    };

    const spy = sinon.spy(user, 'getName');

    user.getName();
    user.getName();

    expect(spy.calledTwice).to.be.true;
  });
});

Copulation HTTP

const nock = require('nock');
const axios = require('axios');
const { expect } = require('chai');

describe('HTTP Mocking', function() {
  afterEach(function() {
    nock.cleanAll();
  });

  it('should mock HTTP requests', async function() {
    nock('https://api.example.com')
      .get('/users/1')
      .reply(200, { id: 1, name: 'John' });

    const response = await axios.get('https://api.example.com/users/1');
    expect(response.data.name).to.equal('John');
  });

  it('should mock POST requests', async function() {
    nock('https://api.example.com')
      .post('/users', { name: 'Jane' })
      .reply(201, { id: 2, name: 'Jane' });

    const response = await axios.post('https://api.example.com/users', {
      name: 'Jane'
    });

    expect(response.status).to.equal(201);
    expect(response.data.id).to.equal(2);
  });

  it('should mock with delays', async function() {
    nock('https://api.example.com')
      .get('/slow')
      .delay(1000)
      .reply(200, { message: 'slow response' });

    const start = Date.now();
    await axios.get('https://api.example.com/slow');
    const duration = Date.now() - start;

    expect(duration).to.be.at.least(1000);
  });
});

Mouvement de module

const proxyquire = require('proxyquire');
const { expect } = require('chai');

describe('Module Mocking', function() {
  it('should mock required modules', function() {
    const dbMock = {
      findUser: sinon.stub().returns({ id: 1, name: 'John' })
    };

    const UserService = proxyquire('../lib/user-service', {
      './database': dbMock
    });

    const user = UserService.getUser(1);
    expect(user.name).to.equal('John');
    expect(dbMock.findUser.calledWith(1)).to.be.true;
  });

  it('should mock with multiple dependencies', function() {
    const mocks = {
      './database': {
        connect: sinon.stub(),
        query: sinon.stub().returns([])
      },
      './logger': {
        log: sinon.stub(),
        error: sinon.stub()
      }
    };

    const Service = proxyquire('../lib/service', mocks);

    Service.initialize();
    expect(mocks['./database'].connect.called).to.be.true;
    expect(mocks['./logger'].log.called).to.be.true;
  });
});

Caractéristiques avancées

Essais parallèles

# Install parallel testing
npm install --save-dev mocha-parallel-tests

# Run tests in parallel
npx mocha-parallel-tests

# Specify max parallel processes
npx mocha-parallel-tests --max-parallel 4

Filtre d'essai

# Run tests matching pattern
npx mocha --grep "should add"

# Run tests NOT matching pattern
npx mocha --grep "should add" --invert

# Run tests in specific files
npx mocha test/unit/**/*.test.js

# Run tests with tags
npx mocha --grep "@slow"
// Tag tests with comments
describe('Calculator', function() {
  it('should add quickly @fast', function() {
    // Fast test
  });

  it('should handle complex calculations @slow', function() {
    // Slow test
  });
});

Génération d'essais dynamiques

describe('Dynamic Tests', function() {
  const testCases = [
    { input: [2, 3], expected: 5 },
    { input: [5, 7], expected: 12 },
    { input: [-1, 1], expected: 0 },
    { input: [0, 0], expected: 0 }
  ];

  testCases.forEach(({ input, expected }) => {
    it(`should add ${input[0]} + ${input[1]} = ${expected}`, function() {
      expect(add(input[0], input[1])).to.equal(expected);
    });
  });
});

// Async dynamic tests
describe('API Endpoints', function() {
  let endpoints;

  before(async function() {
    endpoints = await loadEndpointsConfig();
  });

  function createEndpointTest(endpoint) {
    it(`should respond to ${endpoint.method} ${endpoint.path}`, async function() {
      const response = await request(app)
        [endpoint.method.toLowerCase()](endpoint.path);

      expect(response.status).to.equal(endpoint.expectedStatus);
    });
  }

  // Generate tests after loading config
  before(function() {
    endpoints.forEach(createEndpointTest);
  });
});

Services d'essai

// test/helpers/utils.js
const { expect } = require('chai');

function expectAsync(promise) {
  return {
    toResolve: async () => {
      try {
        const result = await promise;
        return result;
      } catch (error) {
        throw new Error(`Expected promise to resolve, but it rejected with: ${error.message}`);
      }
    },

    toReject: async (expectedError) => {
      try {
        await promise;
        throw new Error('Expected promise to reject, but it resolved');
      } catch (error) {
        if (expectedError) {
          expect(error.message).to.include(expectedError);
        }
        return error;
      }
    }
  };
}

function createUser(overrides = {}) {
  return {
    id: Math.floor(Math.random() * 1000),
    name: 'Test User',
    email: 'test@example.com',
    ...overrides
  };
}

module.exports = { expectAsync, createUser };

Facteurs d'essai

// test/factories/user-factory.js
class UserFactory {
  static create(overrides = {}) {
    return {
      id: this.generateId(),
      name: 'John Doe',
      email: 'john@example.com',
      age: 30,
      active: true,
      createdAt: new Date(),
      ...overrides
    };
  }

  static createMany(count, overrides = {}) {
    return Array.from({ length: count }, (_, index) => 
      this.create({ id: index + 1, ...overrides })
    );
  }

  static generateId() {
    return Math.floor(Math.random() * 10000);
  }
}

module.exports = UserFactory;

// Usage in tests
const UserFactory = require('./factories/user-factory');

describe('User Service', function() {
  it('should process user', function() {
    const user = UserFactory.create({ name: 'Jane' });
    const result = processUser(user);
    expect(result.displayName).to.equal('Jane');
  });

  it('should handle multiple users', function() {
    const users = UserFactory.createMany(5);
    expect(users).to.have.length(5);
  });
});

Greffons

Greffons populaires

# Chai plugins
npm install --save-dev chai-as-promised chai-http chai-string

# Sinon integration
npm install --save-dev sinon-chai

# Coverage
npm install --save-dev nyc

# Parallel testing
npm install --save-dev mocha-parallel-tests

# Multi-reporters
npm install --save-dev mocha-multi-reporters

Utilisation des greffons Chai

const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const chaiHttp = require('chai-http');
const chaiString = require('chai-string');

chai.use(chaiAsPromised);
chai.use(chaiHttp);
chai.use(chaiString);

const { expect } = chai;

describe('Chai Plugins', function() {
  it('should test promises', async function() {
    await expect(Promise.resolve('success')).to.eventually.equal('success');
    await expect(Promise.reject(new Error('fail'))).to.be.rejected;
  });

  it('should test HTTP', function() {
    return chai.request(app)
      .get('/api/users')
      .then(res => {
        expect(res).to.have.status(200);
        expect(res).to.be.json;
        expect(res.body).to.be.an('array');
      });
  });

  it('should test strings', function() {
    expect('Hello World').to.startWith('Hello');
    expect('Hello World').to.endWith('World');
    expect('Hello World').to.equalIgnoreCase('hello world');
  });
});

Développement de plugins personnalisés

// chai-custom-plugin.js
module.exports = function(chai, utils) {
  chai.Assertion.addMethod('validEmail', function() {
    const obj = this._obj;
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

    this.assert(
      emailRegex.test(obj),
      'expected #{this} to be a valid email',
      'expected #{this} not to be a valid email'
    );
  });

  chai.Assertion.addMethod('between', function(min, max) {
    const obj = this._obj;

    this.assert(
      obj >= min && obj <= max,
      'expected #{this} to be between #{exp1} and #{exp2}',
      'expected #{this} not to be between #{exp1} and #{exp2}',
      `${min} and ${max}`
    );
  });
};

// Usage
const chai = require('chai');
chai.use(require('./chai-custom-plugin'));

describe('Custom Plugin', function() {
  it('should validate emails', function() {
    expect('user@example.com').to.be.validEmail;
  });

  it('should check ranges', function() {
    expect(5).to.be.between(1, 10);
  });
});

Intégration CI/CD

Actions GitHub

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16, 18, 20]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run tests
      run: npm test

    - name: Run coverage
      run: npm run test:coverage

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info

Jenkins Pipeline

// Jenkinsfile
pipeline {
    agent any

    tools {
        nodejs '18'
    }

    stages {
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test'
            }
            post {
                always {
                    publishTestResults testResultsPattern: 'test-results.xml'
                }
            }
        }

        stage('Coverage') {
            steps {
                sh 'npm run test:coverage'
            }
            post {
                always {
                    publishHTML([
                        allowMissing: false,
                        alwaysLinkToLastBuild: true,
                        keepAll: true,
                        reportDir: 'coverage',
                        reportFiles: 'index.html',
                        reportName: 'Coverage Report'
                    ])
                }
            }
        }
    }
}

Intégration Docker

# Dockerfile.test
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npm", "test"]
# docker-compose.test.yml
version: '3.8'
services:
  test:
    build:
      context: .
      dockerfile: Dockerfile.test
    environment:
      - NODE_ENV=test
    volumes:
      - ./coverage:/app/coverage

Meilleures pratiques

Organisation des essais

// Good: Clear test structure
describe('UserService', function() {
  describe('#createUser', function() {
    context('with valid data', function() {
      it('should create user successfully', function() {
        // Test implementation
      });

      it('should return user with ID', function() {
        // Test implementation
      });
    });

    context('with invalid data', function() {
      it('should throw validation error', function() {
        // Test implementation
      });
    });
  });
});

// Good: Descriptive test names
it('should return 404 when user does not exist', function() {
  // Test implementation
});

it('should update user email when valid email provided', function() {
  // Test implementation
});

Gestion des données d'essai

// Good: Use factories for test data
const UserFactory = require('./factories/user-factory');

describe('User Tests', function() {
  it('should validate user', function() {
    const user = UserFactory.create({ email: 'invalid-email' });
    expect(() => validateUser(user)).to.throw();
  });
});

// Good: Clean up after tests
describe('Database Tests', function() {
  afterEach(async function() {
    await cleanupDatabase();
  });
});

Meilleures pratiques Async

// Good: Proper async handling
describe('Async Tests', function() {
  it('should handle promises', async function() {
    const result = await asyncOperation();
    expect(result).to.exist;
  });

  it('should handle errors', async function() {
    await expect(failingOperation()).to.be.rejected;
  });
});

// Good: Proper timeout handling
describe('Slow Tests', function() {
  this.timeout(10000);

  it('should complete slow operation', async function() {
    const result = await slowOperation();
    expect(result).to.exist;
  });
});

Déboguement

Mode de débogage

# Run with debug output
DEBUG=mocha* npm test

# Run specific test with debug
npx mocha --grep "specific test" --inspect-brk

# Debug with VS Code
# Add to launch.json
{
  "type": "node",
  "request": "launch",
  "name": "Mocha Debug",
  "program": "${workspaceFolder}/node_modules/.bin/mocha",
  "args": ["--timeout", "999999", "--colors", "${workspaceFolder}/test"],
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}

Déboguage des tests

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

    console.log('Debug data:', data);
    debugger; // Breakpoint for debugging

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

  it('should log test progress', function() {
    console.log('Step 1: Setup');
    const user = createUser();

    console.log('Step 2: Process');
    const result = processUser(user);

    console.log('Step 3: Verify');
    expect(result).to.exist;
  });
});

Erreur de débogage

describe('Error Debugging', function() {
  it('should provide detailed error info', function() {
    try {
      const result = complexOperation();
      expect(result.value).to.equal(42);
    } catch (error) {
      console.error('Test failed with error:', error);
      console.error('Stack trace:', error.stack);
      throw error;
    }
  });
});

Rendement

Efficacité des essais

// Optimize test setup
describe('Performance Tests', function() {
  // Use before/after for expensive setup
  before(async function() {
    this.database = await setupDatabase();
  });

  after(async function() {
    await this.database.close();
  });

  // Avoid unnecessary work in tests
  it('should be fast', function() {
    // Minimal test implementation
    expect(true).to.be.true;
  });
});

Exécution parallèle

# Run tests in parallel
npx mocha-parallel-tests

# Specify parallel workers
npx mocha-parallel-tests --max-parallel 4

Optimisation de la mémoire

// .mocharc.js
module.exports = {
  // Optimize for memory usage
  timeout: 5000,
  bail: true, // Stop on first failure
  exit: true, // Force exit after tests

  // Cleanup after tests
  require: ['test/helpers/cleanup.js']
};

Dépannage

Questions communes

// Issue: Tests not running
// Solution: Check file patterns and paths
// .mocharc.json
{
  "spec": "test/**/*.test.js", // Correct pattern
  "recursive": true
}

// Issue: Async tests timing out
// Solution: Increase timeout or fix async handling
describe('Async Tests', function() {
  this.timeout(10000); // Increase timeout

  it('should handle async', async function() {
    const result = await asyncOperation();
    expect(result).to.exist;
  });
});

// Issue: Memory leaks
// Solution: Proper cleanup
afterEach(function() {
  // Clean up resources
  sinon.restore();
  nock.cleanAll();
});

Configuration de débogage

// Debug configuration issues
console.log('Mocha config:', JSON.stringify(require('./.mocharc.json'), null, 2));

// Debug test discovery
npx mocha --dry-run

// Debug reporter issues
npx mocha --reporter json > test-results.json

Résumé

Mocha est un cadre de test JavaScript flexible et puissant qui fournit:

  • ** Structure flexible**: prise en charge des interfaces BDD, TDD et personnalisées
  • Async Support: Soutien autochtone aux promesses, rappels et async/attendu
  • Rich Ecosystem: vaste plugin écosystème et intégrations
  • ** Environnements multiples**: Exécute dans Node.js et navigateurs
  • Reportage complet: Plusieurs journalistes intégrés et support de journaliste personnalisé
  • Caractéristiques avancées: Essais parallèles, filtrage des essais et génération de tests dynamiques
  • CI/CD Ready: Excellente intégration avec les systèmes d'intégration continue
  • Soutien au débogage: Capacités de débogage riches et rapports d'erreurs

Mocha excelle à fournir une base solide pour les tests JavaScript avec sa flexibilité, son ensemble de fonctionnalités étendu et son écosystème mature. Son approche non avisée permet aux équipes de construire des workflows de test qui répondent à leurs besoins spécifiques tout en conservant une excellente expérience du développeur et une exécution de test fiable.