← All skills

Mocha Skill

Unit testingJavaScriptTypeScript

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill mocha-skill

Playbook

Complete implementation guide with code samples, patterns, and best practices.

Mocha — Advanced Implementation Playbook

§1 — Production Configuration

# .mocharc.yml
spec: 'tests/**/*.test.ts'
timeout: 10000
recursive: true
reporter: spec
require:
  - ts-node/register
  - tests/setup.ts
exit: true
parallel: false
retries: 0
slow: 200
// package.json scripts
{
  "scripts": {
    "test": "mocha",
    "test:watch": "mocha --watch",
    "test:smoke": "mocha --grep @smoke",
    "test:coverage": "nyc mocha",
    "test:ci": "mocha --reporter mocha-junit-reporter --retries 1"
  }
}

NYC (Istanbul) Coverage Config

// .nycrc.json
{
  "extends": "@istanbuljs/nyc-config-typescript",
  "all": true,
  "include": ["src/**/*.ts"],
  "exclude": ["**/*.test.ts", "**/*.d.ts"],
  "reporter": ["text", "lcov", "html"],
  "branches": 80,
  "lines": 85,
  "functions": 85,
  "statements": 85,
  "check-coverage": true
}

TypeScript Setup

// tests/setup.ts
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinonChai from 'sinon-chai';

chai.use(chaiAsPromised);
chai.use(sinonChai);

// Global test timeout
process.env.NODE_ENV = 'test';

§2 — Testing with Chai + Sinon

import { expect } from 'chai';
import sinon, { SinonSandbox, SinonStub } from 'sinon';
import { UserService } from '../src/services/UserService';

describe('UserService', () => {
  let sandbox: SinonSandbox;
  let service: UserService;
  let mockDb: { query: SinonStub; connect: SinonStub };

  beforeEach(() => {
    sandbox = sinon.createSandbox();
    mockDb = {
      query: sandbox.stub(),
      connect: sandbox.stub().resolves(true),
    };
    service = new UserService(mockDb);
  });

  afterEach(() => sandbox.restore());

  describe('create()', () => {
    it('should create user with valid data', async () => {
      mockDb.query.resolves({ id: '1', name: 'Alice', email: 'alice@test.com' });

      const user = await service.create({ name: 'Alice', email: 'alice@test.com' });

      expect(user).to.have.property('id', '1');
      expect(user).to.have.property('name', 'Alice');
      expect(mockDb.query).to.have.been.calledOnce;
      expect(mockDb.query).to.have.been.calledWith(
        sinon.match.string,
        sinon.match.has('name', 'Alice')
      );
    });

    it('should reject invalid email', async () => {
      await expect(service.create({ name: 'Alice', email: 'bad' }))
        .to.be.rejectedWith('Invalid email');
    });

    it('should handle database errors', async () => {
      mockDb.query.rejects(new Error('Connection lost'));
      await expect(service.create({ name: 'Alice', email: 'a@test.com' }))
        .to.be.rejectedWith('Connection lost');
    });
  });

  describe('findById()', () => {
    it('should return null for non-existent user', async () => {
      mockDb.query.resolves(null);
      const user = await service.findById('999');
      expect(user).to.be.null;
    });
  });
});

§3 — Advanced Sinon Patterns

// Stubs with sequential returns
const stub = sandbox.stub();
stub.onFirstCall().resolves({ page: 1, data: [1, 2] });
stub.onSecondCall().resolves({ page: 2, data: [3, 4] });
stub.onThirdCall().resolves({ page: 3, data: [] });

// Fake timers
describe('Token Expiry', () => {
  let clock: sinon.SinonFakeTimers;

  beforeEach(() => { clock = sinon.useFakeTimers(); });
  afterEach(() => { clock.restore(); });

  it('should expire token after 1 hour', async () => {
    const token = await auth.createToken('user');
    expect(auth.isValid(token)).to.be.true;
    clock.tick(3600 * 1000 + 1);  // advance 1 hour + 1ms
    expect(auth.isValid(token)).to.be.false;
  });
});

// Spy on existing methods
it('should log on error', async () => {
  const logSpy = sandbox.spy(logger, 'error');
  mockDb.query.rejects(new Error('fail'));
  try { await service.create(validData); } catch {}
  expect(logSpy).to.have.been.calledOnce;
  expect(logSpy).to.have.been.calledWith(sinon.match('fail'));
});

// Fake server (HTTP)
import nock from 'nock';

describe('API Client', () => {
  afterEach(() => nock.cleanAll());

  it('should fetch users', async () => {
    nock('https://api.example.com')
      .get('/users')
      .reply(200, [{ id: 1, name: 'Alice' }]);

    const users = await apiClient.getUsers();
    expect(users).to.have.lengthOf(1);
    expect(users[0].name).to.equal('Alice');
  });

  it('should handle 500 errors', async () => {
    nock('https://api.example.com')
      .get('/users')
      .reply(500, { error: 'Internal Server Error' });

    await expect(apiClient.getUsers()).to.be.rejectedWith('Server error');
  });
});

§4 — Async Patterns

// Promises
it('should resolve with data', () => {
  return fetchData().then(data => expect(data).to.exist);
});

// Async/await
it('should resolve with data', async () => {
  const data = await fetchData();
  expect(data).to.exist;
});

// Callbacks (done)
it('should callback with data', (done) => {
  fetchDataCallback((err, data) => {
    if (err) return done(err);
    expect(data).to.exist;
    done();
  });
});

// Event emitters
it('should emit "data" event', (done) => {
  const emitter = createStream();
  emitter.on('data', (chunk) => {
    expect(chunk).to.be.a('string');
    done();
  });
  emitter.start();
});

// Timeout override per test
it('should complete long operation', async function() {
  this.timeout(30000);
  const result = await longRunningOperation();
  expect(result).to.equal('complete');
});

§5 — Hooks & Test Organization

// Lifecycle hooks
describe('Database Tests', () => {
  let connection: DbConnection;

  before(async () => {
    // Runs once before all tests in this describe
    connection = await Database.connect(TEST_DB_URL);
  });

  after(async () => {
    // Runs once after all tests
    await connection.close();
  });

  beforeEach(async () => {
    // Runs before each test
    await connection.truncateAll();
    await connection.seed('users', testUsers);
  });

  afterEach(async () => {
    // Runs after each test
    await connection.rollback();
  });

  // Tests...
});

// Nested describes
describe('Cart', () => {
  describe('when empty', () => {
    it('should have zero total', () => { /* ... */ });
    it('should show empty message', () => { /* ... */ });
  });

  describe('with items', () => {
    beforeEach(() => { cart.add(item1); cart.add(item2); });

    it('should calculate total', () => { /* ... */ });
    it('should apply discount', () => { /* ... */ });

    describe('at checkout', () => {
      it('should validate stock', () => { /* ... */ });
    });
  });
});

// Skip and only
describe.skip('WIP Feature', () => { /* skipped */ });
it.only('debug this test', () => { /* only this runs */ });

// Grep tags in test names
it('should login @smoke', () => { /* run with --grep @smoke */ });
it('should process order @regression', () => { /* ... */ });

§6 — Custom Reporters & Plugins

// Custom reporter
class CustomReporter {
  constructor(runner: Mocha.Runner) {
    let passes = 0, failures = 0;

    runner.on('pass', (test) => {
      passes++;
      console.log(`✓ ${test.fullTitle()} (${test.duration}ms)`);
    });

    runner.on('fail', (test, err) => {
      failures++;
      console.log(`✗ ${test.fullTitle()}: ${err.message}`);
    });

    runner.on('end', () => {
      console.log(`\n${passes} passing, ${failures} failing`);
    });
  }
}

// Root-level hooks (runs for all test files)
// tests/hooks.ts
export const mochaHooks = {
  beforeAll() { console.log('Suite starting'); },
  afterAll() { console.log('Suite complete'); },
  beforeEach() { /* ... */ },
  afterEach() { /* ... */ },
};

§7 — Express/API Testing with Supertest

import request from 'supertest';
import { expect } from 'chai';
import app from '../src/app';

describe('User API', () => {
  let token: string;

  before(async () => {
    const res = await request(app)
      .post('/api/auth/login')
      .send({ email: 'admin@test.com', password: 'admin123' });
    token = res.body.token;
  });

  describe('GET /api/users', () => {
    it('should return users list', async () => {
      const res = await request(app)
        .get('/api/users')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);

      expect(res.body).to.be.an('array');
      expect(res.body[0]).to.have.property('name');
    });

    it('should reject without auth', async () => {
      await request(app).get('/api/users').expect(401);
    });
  });

  describe('POST /api/users', () => {
    it('should create user', async () => {
      const res = await request(app)
        .post('/api/users')
        .set('Authorization', `Bearer ${token}`)
        .send({ name: 'New User', email: 'new@test.com' })
        .expect(201);

      expect(res.body).to.have.property('id');
    });

    it('should validate required fields', async () => {
      const res = await request(app)
        .post('/api/users')
        .set('Authorization', `Bearer ${token}`)
        .send({})
        .expect(400);

      expect(res.body.errors).to.have.lengthOf.at.least(1);
    });
  });
});

§8 — CI/CD Integration

# GitHub Actions
name: Mocha Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20]
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_DB: test, POSTGRES_PASSWORD: test }
        ports: ['5432:5432']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: ${{ matrix.node-version }} }
      - run: npm ci
      - run: npm run test:ci
        env:
          DATABASE_URL: postgres://postgres:test@localhost:5432/test
          CI: true
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results-node${{ matrix.node-version }}
          path: |
            test-results/
            coverage/

§9 — Debugging Quick-Reference

ProblemCauseFix
Tests hang (no exit)Open DB connections or timersUse --exit flag or cleanup in after() hooks
Timeout exceededSlow async opsIncrease: this.timeout(30000) per test
Stub not workingWrong import or module cachingUse sinon.stub(object, 'method'), check import path
afterEach not restoringMissing sandbox.restore()Always call in afterEach, use sandbox pattern
Async error swallowedMissing await or returnAlways return promise or use async/await
Test order dependencyShared mutable stateReset in beforeEach, use separate describes
--watch not rerunningFile not matching spec patternCheck .mocharc.yml spec glob
Coverage shows 0%Source not instrumentedUse nyc wrapper: nyc mocha
Nock not interceptingURL mismatch or https vs httpMatch exact URL including protocol
describe.only left in codeForgotten debugging flagUse lint rule: no-only-tests

§10 — Best Practices Checklist

  • ✅ Use sinon.createSandbox() + sandbox.restore() in afterEach
  • ✅ Use Chai's expect style for readable assertions
  • ✅ Use chai-as-promised for async assertion chains
  • ✅ Use --exit flag to prevent hanging from open handles
  • ✅ Use nock for HTTP mocking (not sinon for fetch/axios)
  • ✅ Use nyc for coverage with threshold enforcement
  • ✅ Use grep tags (@smoke, @regression) for selective runs
  • ✅ Use root-level hooks (mochaHooks) for global setup/teardown
  • ✅ Use nested describe blocks for logical grouping
  • ✅ Clean up all stubs, mocks, and connections in afterEach/after
  • ✅ Use supertest for Express API testing
  • ✅ Use TypeScript with ts-node/register for type safety
  • ✅ Structure: tests/unit/, tests/integration/, tests/fixtures/, tests/setup.ts