Qaskills Mocha Testing
Comprehensive Mocha testing skill for writing robust unit and integration tests in JavaScript and TypeScript with Chai assertions, Sinon mocking, async patterns, and CI/CD integration.
install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/mocha-testing" ~/.claude/skills/pramoddutta-qaskills-mocha-testing && rm -rf "$T"
manifest:
seed-skills/mocha-testing/SKILL.mdsource content
Mocha Testing
You are an expert QA engineer specializing in Mocha-based testing with Chai assertions and Sinon mocking. When the user asks you to write, review, debug, or set up Mocha-related tests or configurations, follow these detailed instructions.
Core Principles
- BDD-Style Structure -- Use Mocha's
/describe
blocks to organize tests in a behavior-driven style. Eachit
groups related tests; eachdescribe
asserts a single behavior.it - Chai Assertion Clarity -- Use Chai's
style for readable assertions. Prefer specific matchers (expect
,to.equal
,to.deep.equal
) over generic ones (to.include
).to.be.ok - Sinon Isolation -- Use Sinon stubs, spies, and mocks to isolate units under test. Always restore stubs in
using sandboxes to prevent test pollution.afterEach - Async Test Patterns -- Handle asynchronous code with
, returning promises, or Mocha'sasync/await
callback. Never mix approaches within a single test.done - Lifecycle Hook Discipline -- Use
for one-time setup,before
for per-test setup,beforeEach
for cleanup, andafterEach
for teardown. Keep hooks focused and minimal.after - Test Independence -- Every test must pass when run alone or in any order. Never rely on shared mutable state or side effects from other tests.
- Descriptive Naming -- Write test names that describe the expected behavior:
rather than'should return 404 when user is not found'
.'test not found'
When to Use This Skill
- When writing unit tests for JavaScript/TypeScript modules, functions, or classes
- When testing Express.js or Node.js API endpoints
- When setting up Mocha with Chai and Sinon for a project
- When debugging failing or flaky Mocha tests
- When configuring Mocha for CI/CD pipelines
- When testing async operations (promises, callbacks, event emitters)
- When working with
,describe
,it
,expect
, orsinon.stub.mocharc.yml
Project Structure
project-root/ ├── .mocharc.yml # Mocha configuration ├── src/ │ ├── services/ │ │ ├── user.service.ts │ │ ├── auth.service.ts │ │ └── payment.service.ts │ ├── models/ │ │ └── user.model.ts │ ├── utils/ │ │ └── validators.ts │ └── app.ts ├── test/ │ ├── unit/ # Unit tests │ │ ├── services/ │ │ │ ├── user.service.test.ts │ │ │ ├── auth.service.test.ts │ │ │ └── payment.service.test.ts │ │ └── utils/ │ │ └── validators.test.ts │ ├── integration/ # Integration tests │ │ ├── api/ │ │ │ ├── users.api.test.ts │ │ │ └── auth.api.test.ts │ │ └── database/ │ │ └── user.repo.test.ts │ ├── fixtures/ # Test data │ │ ├── users.fixture.ts │ │ └── products.fixture.ts │ ├── helpers/ # Shared test utilities │ │ ├── setup.ts │ │ └── factories.ts │ └── mocha.setup.ts # Global test setup ├── coverage/ # Coverage reports └── package.json
Configuration
.mocharc.yml
require: - ts-node/register - test/mocha.setup.ts spec: 'test/**/*.test.ts' recursive: true timeout: 10000 reporter: spec exit: true
mocha.setup.ts
import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import sinonChai from 'sinon-chai'; chai.use(chaiAsPromised); chai.use(sinonChai); // Global hooks before(function () { console.log('Test suite starting...'); }); after(function () { console.log('Test suite complete.'); });
Chai Assertion Patterns
Equality and Type Checks
import { expect } from 'chai'; describe('Chai Assertions', () => { it('should check equality', () => { expect(42).to.equal(42); expect('hello').to.equal('hello'); expect({ a: 1 }).to.deep.equal({ a: 1 }); // Deep comparison expect([1, 2, 3]).to.deep.equal([1, 2, 3]); }); it('should check types', () => { expect('hello').to.be.a('string'); expect(42).to.be.a('number'); expect(true).to.be.a('boolean'); expect([]).to.be.an('array'); expect({}).to.be.an('object'); expect(null).to.be.null; expect(undefined).to.be.undefined; }); it('should check inclusion', () => { expect('hello world').to.include('world'); expect([1, 2, 3]).to.include(2); expect({ a: 1, b: 2 }).to.include({ a: 1 }); expect([{ id: 1 }, { id: 2 }]).to.deep.include({ id: 1 }); }); it('should check numeric ranges', () => { expect(10).to.be.above(5); expect(10).to.be.below(20); expect(10).to.be.at.least(10); expect(10).to.be.at.most(10); expect(10).to.be.within(5, 15); }); it('should check object properties', () => { const user = { name: 'Alice', age: 30, role: 'admin' }; expect(user).to.have.property('name'); expect(user).to.have.property('name', 'Alice'); expect(user).to.have.all.keys('name', 'age', 'role'); expect(user).to.have.any.keys('name', 'email'); }); it('should check exceptions', () => { const throwError = () => { throw new Error('Something broke'); }; expect(throwError).to.throw(Error); expect(throwError).to.throw('Something broke'); expect(throwError).to.throw(/broke/); }); });
Sinon Mocking Patterns
Stubs and Spies
import { expect } from 'chai'; import sinon, { SinonSandbox } from 'sinon'; import { UserService } from '../../src/services/user.service'; import { UserRepository } from '../../src/repositories/user.repository'; describe('UserService', () => { let sandbox: SinonSandbox; let userService: UserService; let userRepoStub: sinon.SinonStubbedInstance<UserRepository>; beforeEach(() => { sandbox = sinon.createSandbox(); userRepoStub = sandbox.createStubInstance(UserRepository); userService = new UserService(userRepoStub as any); }); afterEach(() => { sandbox.restore(); // Critical: always restore stubs }); describe('getUser', () => { it('should return user when found', async () => { const mockUser = { id: '1', name: 'Alice', email: 'alice@example.com' }; userRepoStub.findById.resolves(mockUser); const result = await userService.getUser('1'); expect(result).to.deep.equal(mockUser); expect(userRepoStub.findById).to.have.been.calledOnceWith('1'); }); it('should throw when user not found', async () => { userRepoStub.findById.resolves(null); await expect(userService.getUser('999')).to.be.rejectedWith('User not found'); }); it('should propagate repository errors', async () => { userRepoStub.findById.rejects(new Error('Database connection failed')); await expect(userService.getUser('1')).to.be.rejectedWith('Database connection failed'); }); }); describe('createUser', () => { it('should create user with hashed password', async () => { const userData = { name: 'Bob', email: 'bob@example.com', password: 'plaintext123' }; const createdUser = { id: '2', name: 'Bob', email: 'bob@example.com' }; userRepoStub.findByEmail.resolves(null); userRepoStub.create.resolves(createdUser); const result = await userService.createUser(userData); expect(result).to.deep.equal(createdUser); expect(userRepoStub.create).to.have.been.calledOnce; // Verify password was not stored in plain text const createCall = userRepoStub.create.firstCall; expect(createCall.args[0].password).to.not.equal('plaintext123'); }); it('should reject duplicate emails', async () => { userRepoStub.findByEmail.resolves({ id: '1', email: 'bob@example.com' }); await expect( userService.createUser({ name: 'Bob', email: 'bob@example.com', password: 'pass' }) ).to.be.rejectedWith('Email already exists'); }); }); });
Spying on Callbacks and Events
import { expect } from 'chai'; import sinon from 'sinon'; import { EventEmitter } from 'events'; describe('Event Handling', () => { it('should emit events in correct order', () => { const emitter = new EventEmitter(); const spy = sinon.spy(); emitter.on('data', spy); emitter.emit('data', { id: 1 }); emitter.emit('data', { id: 2 }); expect(spy).to.have.been.calledTwice; expect(spy.firstCall).to.have.been.calledWith({ id: 1 }); expect(spy.secondCall).to.have.been.calledWith({ id: 2 }); }); it('should spy on method calls', () => { const calculator = { add: (a: number, b: number) => a + b, }; const spy = sinon.spy(calculator, 'add'); const result = calculator.add(2, 3); expect(result).to.equal(5); expect(spy).to.have.been.calledOnceWith(2, 3); expect(spy).to.have.returned(5); spy.restore(); }); });
Fake Timers
import { expect } from 'chai'; import sinon from 'sinon'; describe('Timer-Based Functions', () => { let clock: sinon.SinonFakeTimers; beforeEach(() => { clock = sinon.useFakeTimers(); }); afterEach(() => { clock.restore(); }); it('should debounce function calls', () => { const callback = sinon.spy(); function debounce(fn: Function, delay: number) { let timer: NodeJS.Timeout; return (...args: any[]) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } const debounced = debounce(callback, 300); debounced('a'); debounced('b'); debounced('c'); expect(callback).to.not.have.been.called; clock.tick(300); expect(callback).to.have.been.calledOnceWith('c'); }); it('should handle retry with exponential backoff', async () => { const apiCall = sinon.stub(); apiCall.onFirstCall().rejects(new Error('Timeout')); apiCall.onSecondCall().rejects(new Error('Timeout')); apiCall.onThirdCall().resolves({ data: 'success' }); // Assume retryWithBackoff calls setTimeout between retries // Test verifies the timing and eventual success }); });
Testing Express.js APIs
import { expect } from 'chai'; import request from 'supertest'; import express from 'express'; import sinon from 'sinon'; describe('Users API', () => { let app: express.Application; let sandbox: sinon.SinonSandbox; beforeEach(() => { sandbox = sinon.createSandbox(); app = createApp(); // Factory function that creates fresh Express app }); afterEach(() => { sandbox.restore(); }); describe('GET /api/users', () => { it('should return all users', async () => { const res = await request(app).get('/api/users').expect(200); expect(res.body).to.be.an('array'); expect(res.body.length).to.be.greaterThan(0); expect(res.body[0]).to.have.property('id'); expect(res.body[0]).to.have.property('name'); expect(res.body[0]).to.have.property('email'); }); it('should paginate results', async () => { const res = await request(app).get('/api/users?page=1&limit=5').expect(200); expect(res.body).to.be.an('array'); expect(res.body.length).to.be.at.most(5); }); }); describe('POST /api/users', () => { it('should create a new user', async () => { const newUser = { name: 'Alice', email: 'alice@example.com', password: 'Secure123!' }; const res = await request(app).post('/api/users').send(newUser).expect(201); expect(res.body).to.have.property('id'); expect(res.body.name).to.equal('Alice'); expect(res.body.email).to.equal('alice@example.com'); expect(res.body).to.not.have.property('password'); }); it('should return 400 for invalid email', async () => { const res = await request(app) .post('/api/users') .send({ name: 'Bob', email: 'not-an-email', password: 'pass' }) .expect(400); expect(res.body).to.have.property('error'); expect(res.body.error).to.include('email'); }); it('should return 409 for duplicate email', async () => { const user = { name: 'Alice', email: 'existing@example.com', password: 'Secure123!' }; await request(app).post('/api/users').send(user); const res = await request(app).post('/api/users').send(user).expect(409); expect(res.body.error).to.include('already exists'); }); }); describe('GET /api/users/:id', () => { it('should return 404 for non-existent user', async () => { const res = await request(app).get('/api/users/nonexistent-id').expect(404); expect(res.body).to.have.property('error'); }); }); });
Async Test Patterns
import { expect } from 'chai'; describe('Async Patterns', () => { // Pattern 1: async/await (preferred) it('should handle async with await', async () => { const result = await fetchData(); expect(result).to.deep.equal({ status: 'ok' }); }); // Pattern 2: returning a promise it('should handle returned promise', () => { return fetchData().then((result) => { expect(result).to.deep.equal({ status: 'ok' }); }); }); // Pattern 3: done callback (for legacy code) it('should handle done callback', (done) => { fetchDataCallback((err, result) => { try { expect(err).to.be.null; expect(result).to.deep.equal({ status: 'ok' }); done(); } catch (e) { done(e); } }); }); // Pattern 4: chai-as-promised it('should assert on rejected promises', async () => { await expect(failingOperation()).to.be.rejectedWith(Error, 'Something failed'); }); });
Best Practices
- Use Sinon sandboxes -- Always create a sandbox in
and restore it inbeforeEach
. This prevents stub leakage between tests.afterEach - Prefer
style overexpect
orassert
for consistency. Chai'sshould
provides the best TypeScript support and readability.expect - Use
for async tests -- This is the most readable pattern and provides clear stack traces on failure. Avoid mixing withasync/await
callbacks.done - Keep tests focused -- Each
block should test one specific behavior. If a test name contains "and", split it into separate tests.it - Use descriptive
nesting -- Nestdescribe
blocks to organize by method/feature and scenario:describe
.describe('createUser') > describe('with valid data') - Use
for asserting on promise rejections.chai-as-promised
is cleaner than try/catch patterns.expect(promise).to.be.rejectedWith() - Create test fixtures as factory functions that return fresh data for each test, avoiding shared mutable objects.
- Run tests in watch mode during development with
for instant feedback on code changes.mocha --watch - Configure timeouts appropriately -- Set global timeout in
and override per-test with.mocharc.yml
for slow operations.this.timeout() - Use
flag in CI to force Mocha to exit after tests complete, preventing hanging processes from open handles.--exit
Anti-Patterns
- Not restoring Sinon stubs -- Leaked stubs affect subsequent tests and cause cryptic failures. Always use sandboxes or explicit
..restore() - Using arrow functions in
/describe
-- Arrow functions bindit
lexically, breaking Mocha's context features likethis
andthis.timeout()
.this.retries() - Mixing async patterns -- Using both
callback and returning a promise in the same test causes unpredictable behavior.done - Forgetting
in callbacks -- Not callingdone(error)
with the error in catch blocks makes tests time out instead of failing immediately.done() - Sharing mutable state between tests -- Modifying objects defined in outer scopes causes ordering-dependent test failures.
- Testing implementation details -- Asserting on internal method calls rather than observable behavior makes tests brittle to refactoring.
- Not using
for objects -- Usingdeep.equal
for object comparison checks reference equality, not value equality. Useequal
for structural comparison.deep.equal - Skipping error path testing -- Only testing happy paths leaves error handling untested. Always test invalid inputs, missing data, and failure scenarios.
- Using
to disable timeouts -- This masks tests that hang indefinitely. Set a generous but finite timeout instead.this.timeout(0) - Not using
flag -- Forgetting to recurse into subdirectories means tests in nested folders are silently skipped.--recursive
CLI Reference
# Run all tests npx mocha # Run specific file npx mocha test/unit/services/user.service.test.ts # Run tests matching pattern npx mocha --grep "should create user" # Run in watch mode npx mocha --watch # Run with timeout npx mocha --timeout 15000 # Run with specific reporter npx mocha --reporter dot npx mocha --reporter json > results.json # Run recursive npx mocha --recursive test/ # Run with coverage (nyc/istanbul) npx nyc mocha # Run with bail (stop on first failure) npx mocha --bail
Setup
# Install Mocha with Chai and Sinon npm install --save-dev mocha chai sinon # TypeScript support npm install --save-dev ts-node typescript @types/mocha @types/chai @types/sinon # Chai plugins npm install --save-dev chai-as-promised sinon-chai npm install --save-dev @types/chai-as-promised @types/sinon-chai # Coverage npm install --save-dev nyc # API testing npm install --save-dev supertest @types/supertest # Create config echo 'require: ts-node/register\nspec: "test/**/*.test.ts"\nrecursive: true\ntimeout: 10000' > .mocharc.yml