# SkillPatch skill: mocha-skill

This skill enables agents to generate Mocha tests in JavaScript and TypeScript using Chai assertions and Sinon mocking. It provides ready-to-use code patterns for basic tests, async testing, stubbing/spying, and anti-pattern guidance. It also includes a quick-reference command table and pointers to deeper playbook sections covering CI/CD, Express/API testing, and more.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/mocha-skill
curl -sSL https://skillpatch.dev/install_skill/mocha-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `reference/advanced-patterns.md`
- `reference/playbook.md`


### `SKILL.md`

````markdown
---
name: mocha-skill
description: >
  Generates Mocha tests in JavaScript with Chai assertions and Sinon mocking.
  Use when user mentions "Mocha", "Chai", "sinon", "describe/it (not Jest)".
  Triggers on: "Mocha", "Chai", "sinon", "mocha test".
languages:
  - JavaScript
  - TypeScript
category: unit-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# Mocha Testing Skill

## Core Patterns

### Basic Test with Chai

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

describe('Calculator', () => {
  let calc;
  beforeEach(() => { calc = new Calculator(); });

  it('should add two numbers', () => {
    expect(calc.add(2, 3)).to.equal(5);
  });

  it('should throw on divide by zero', () => {
    expect(() => calc.divide(10, 0)).to.throw('Division by zero');
  });
});
```

### Chai Assertions

```javascript
expect(value).to.equal(5);
expect(arr).to.have.lengthOf(3);
expect(obj).to.have.property('name');
expect(str).to.include('hello');
expect(fn).to.throw(Error);
expect(arr).to.deep.equal([1, 2, 3]);
expect(obj).to.deep.include({ name: 'Alice' });
```

### Sinon Mocking

```javascript
const sinon = require('sinon');

describe('UserService', () => {
  let sandbox;
  beforeEach(() => { sandbox = sinon.createSandbox(); });
  afterEach(() => { sandbox.restore(); });

  it('fetches user from API', async () => {
    const stub = sandbox.stub(api, 'get').resolves({ name: 'Alice' });
    const user = await userService.getUser(1);
    expect(user.name).to.equal('Alice');
    expect(stub.calledOnce).to.be.true;
  });
});
```

### Async Testing

```javascript
it('should fetch data', async () => {
  const data = await fetchData();
  expect(data).to.have.property('id');
});

it('callback style', (done) => {
  fetchData((err, data) => {
    expect(err).to.be.null;
    done();
  });
});
```

### Anti-Patterns

| Bad | Good | Why |
|-----|------|-----|
| Missing `done()` | Use async/await | Hanging tests |
| No sandbox | `sinon.createSandbox()` | Stubs leak |
| Arrow in `describe` | Regular function for `this.timeout()` | Context |

## Quick Reference

| Task | Command |
|------|---------|
| Run all | `npx mocha` |
| Watch | `npx mocha --watch` |
| Grep | `npx mocha --grep "login"` |
| Timeout | `npx mocha --timeout 10000` |
| Recursive | `npx mocha --recursive` |

## Setup: `npm install mocha chai sinon --save-dev`

## Deep Patterns → `reference/playbook.md`

| § | Section | Lines |
|---|---------|-------|
| 1 | Production Configuration | mocharc, NYC coverage, TypeScript |
| 2 | Testing with Chai + Sinon | Stubs, spies, assertions |
| 3 | Advanced Sinon Patterns | Fake timers, nock HTTP, sequential |
| 4 | Async Patterns | Promise, await, callback, events |
| 5 | Hooks & Test Organization | Lifecycle, nesting, grep tags |
| 6 | Custom Reporters & Plugins | Reporter class, root hooks |
| 7 | Express/API Testing | Supertest integration |
| 8 | CI/CD Integration | GitHub Actions, services |
| 9 | Debugging Quick-Reference | 10 common problems |
| 10 | Best Practices Checklist | 13 items |

````


### `reference/advanced-patterns.md`

````markdown
# Mocha — Advanced Patterns & Playbook

## Async Patterns

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

describe('Async Operations', () => {
  // Promise-based
  it('fetches user data', () => {
    return fetchUser(1).then(user => {
      expect(user).to.have.property('name');
      expect(user.name).to.be.a('string');
    });
  });

  // Async/await
  it('creates and retrieves user', async () => {
    const created = await createUser({ name: 'Alice' });
    const fetched = await fetchUser(created.id);
    expect(fetched).to.deep.include({ name: 'Alice' });
  });

  // Callback (done)
  it('emits event', (done) => {
    const emitter = new EventEmitter();
    emitter.on('data', (val) => {
      expect(val).to.equal('hello');
      done();
    });
    emitter.emit('data', 'hello');
  });

  // Retry flaky tests
  it('handles intermittent failures', async function () {
    this.retries(3);
    const result = await unreliableService.call();
    expect(result.status).to.equal('ok');
  });
});
```

## Sinon Mocking & Stubbing

```javascript
describe('Service Layer', () => {
  let sandbox;
  beforeEach(() => { sandbox = sinon.createSandbox(); });
  afterEach(() => sandbox.restore());

  it('calls API with correct params', async () => {
    const stub = sandbox.stub(http, 'get').resolves({ data: { id: 1 } });
    await userService.getUser(1);
    expect(stub).to.have.been.calledOnceWith('/api/users/1');
  });

  it('handles API errors gracefully', async () => {
    sandbox.stub(http, 'get').rejects(new Error('500'));
    const result = await userService.getUser(1);
    expect(result).to.be.null;
  });

  // Fake timers
  it('debounces calls', () => {
    const clock = sandbox.useFakeTimers();
    const spy = sandbox.spy();
    const debounced = debounce(spy, 300);
    debounced(); debounced(); debounced();
    clock.tick(300);
    expect(spy).to.have.been.calledOnce;
  });

  // Spy on prototype
  it('logs on error', async () => {
    const logSpy = sandbox.spy(Logger.prototype, 'error');
    sandbox.stub(http, 'get').rejects(new Error('fail'));
    await userService.getUser(1);
    expect(logSpy).to.have.been.calledOnce;
  });
});
```

## Chai Assertion Patterns

```javascript
// Deep equality
expect({ a: { b: [1, 2] } }).to.deep.equal({ a: { b: [1, 2] } });

// Partial matching
expect(user).to.include({ name: 'Alice' });
expect(users).to.deep.include.members([{ id: 1 }, { id: 2 }]);

// Type checking
expect(result).to.be.an('array').that.has.lengthOf(3);
expect(fn).to.be.a('function');

// chai-as-promised
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
await expect(fetchUser(-1)).to.be.rejectedWith(Error, /not found/);

// chai-http for API testing
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
const res = await chai.request(app).get('/api/users').set('Authorization', 'Bearer token');
expect(res).to.have.status(200);
expect(res.body).to.be.an('array');
```

## Hooks & Lifecycle

```javascript
describe('Database Tests', () => {
  before(async () => { await db.connect(); await db.migrate(); });       // Once before all
  after(async () => { await db.disconnect(); });                          // Once after all
  beforeEach(async () => { await db.seed(); });                           // Before each test
  afterEach(async () => { await db.truncate(); });                        // After each test

  it('inserts record', async () => { /* ... */ });
});

// Root hooks plugin (.mocharc.yml: require: ./test/hooks.js)
module.exports = {
  mochaHooks: {
    beforeAll() { console.log('Global setup'); },
    afterAll() { console.log('Global teardown'); },
    beforeEach() { /* per-test setup */ },
    afterEach() { /* per-test cleanup */ }
  }
};
```

## Configuration

```yaml
# .mocharc.yml — production-grade
spec: 'test/**/*.spec.{js,ts}'
require:
  - ts-node/register
  - test/hooks.js
timeout: 10000
retries: 1
recursive: true
reporter: mochawesome
reporter-options:
  reportDir: reports
  reportFilename: test-report
  charts: true
  inline: true
parallel: true
jobs: 4
exit: true
```

## Custom Reporter

```javascript
class CustomReporter {
  constructor(runner) {
    const stats = runner.stats;
    runner.on('pass', test => console.log(`✅ ${test.fullTitle()} (${test.duration}ms)`));
    runner.on('fail', (test, err) => console.log(`❌ ${test.fullTitle()}: ${err.message}`));
    runner.on('end', () => console.log(`\n${stats.passes} passing, ${stats.failures} failing`));
  }
}
module.exports = CustomReporter;
```

## Anti-Patterns

- ❌ Arrow functions in `describe`/`it` — loses `this` context for `this.timeout()`, `this.retries()`
- ❌ Missing `afterEach` cleanup — leads to leaky state between tests
- ❌ `done()` with promises — pick one async style per test, not both
- ❌ Hardcoded timeouts in tests — use `.timeout()` or config, not `setTimeout`
- ❌ Tests dependent on execution order — each test must be independently runnable

````


### `reference/playbook.md`

````markdown
# Mocha — Advanced Implementation Playbook

## §1 — Production Configuration

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

```json
// 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

```json
// .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

```typescript
// 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

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
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

```yaml
# 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

| Problem | Cause | Fix |
|---------|-------|-----|
| Tests hang (no exit) | Open DB connections or timers | Use `--exit` flag or cleanup in `after()` hooks |
| Timeout exceeded | Slow async ops | Increase: `this.timeout(30000)` per test |
| Stub not working | Wrong import or module caching | Use `sinon.stub(object, 'method')`, check import path |
| `afterEach` not restoring | Missing `sandbox.restore()` | Always call in `afterEach`, use sandbox pattern |
| Async error swallowed | Missing `await` or `return` | Always `return` promise or use `async/await` |
| Test order dependency | Shared mutable state | Reset in `beforeEach`, use separate describes |
| `--watch` not rerunning | File not matching spec pattern | Check `.mocharc.yml` spec glob |
| Coverage shows 0% | Source not instrumented | Use `nyc` wrapper: `nyc mocha` |
| Nock not intercepting | URL mismatch or https vs http | Match exact URL including protocol |
| `describe.only` left in code | Forgotten debugging flag | Use 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`

````
