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.
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
## 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.