Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Mocha Mastery
Mocha is the “Swiss Army Knife” of JavaScript testing. Unlike Jest, which is an all-in-one framework (Runner + Assertions + Mocks), Mocha is primarily a Test Runner. Think of it like buying a gaming PC as individual components versus buying a pre-built one: Mocha lets you pick your own assertion library, mock library, and coverage tool, giving maximum flexibility at the cost of more assembly. This makes Mocha particularly popular in teams that have strong opinions about their toolchain or need specialized integrations.- Runner: Mocha
- Assertions: Chai (Expect/Should)
- Mocks: Sinon
- Coverage: NYC (Istanbul)
1. The Mocha Runtime
Understanding how Mocha executes files is crucial for avoiding “it works on my machine” errors. Mocha’s two-phase execution model is the single most misunderstood aspect of the framework, and it causes the majority of confusing test failures.Discovery Phase vs Execution Phase
When you runmocha, it does not execute your tests top-to-bottom like a regular script. Instead, it uses two distinct phases:
- Discovery: Mocha scans directories for test files matching the configured pattern.
- Parsing: It
require()s every test file.describe()blocks execute IMMEDIATELY at this stage — they are just function calls that register suites and tests. - Execution: Only after all files are parsed does Mocha run
it(),before(),after()hooks in the order it discovered them.
before() hooks.
Dynamic Test Generation
Sincedescribe runs during parsing, you can generate tests dynamically:
2. Advanced Assertions (Chai)
Chai offers three assertion styles:expect, should, and assert. The expect style is the most popular because it reads like natural English and works consistently across all environments (unlike should, which modifies Object.prototype).
Deep Equality vs Strict Equality
This distinction trips up nearly every beginner. JavaScript has two notions of “equal” for objects, and Chai exposes both:Chaining
Chai chains are readable English.Plugins
Chai functionality is extended via plugins.chai-as-promised:await expect(promise).to.be.rejectedWith(Error)chai-http: Integration testing for API endpoints.
3. Spies, Stubs, and Mocks (Sinon)
Sinon is powerful but confusing because it offers three things that sound similar: spies, stubs, and mocks. Think of them on a spectrum of control:- Spy = Security camera (watches, does not interfere)
- Stub = Stunt double (replaces behavior entirely)
- Mock = Strict contract (expects specific calls, fails if not met)
1. Spies (Observation)
Use when you don’t want to change behavior, just verify that a function was called, how many times, and with what arguments.2. Stubs (Control)
Use when you want to force behavior — making a function return a specific value, throw an error, or behave differently on successive calls. Stubs completely replace the original function.3. Mocks (Expectation Manager)
Combines spying and verification. “This method MUST be called X times”.4. Sinon Sandbox
Manually restoring every stub is painful and error-prone — miss onerestore() call and you have a “test pollution” bug that only surfaces when tests run in a specific order. Sandboxes solve this by tracking every spy/stub/mock you create and restoring them all in one call.
4. Async & Time
Handling Timeouts
Default timeout is 2000ms. For slow integration tests:- Global Command:
mocha --timeout 10000 - Per Suite:
this.timeout(5000)inside describe. - Per Test:
this.timeout(5000)inside it.
Fake Timers (Sinon)
Don’t wait 5 seconds in a unit test.5. Root Hooks & Global Fixtures
For setting up Database connections or Web Servers once for the entire suite. Create a filetest/hooks.js:
mocha --require test/hooks.js
6. Code Coverage (NYC)
Istanbul (NYC) instruments your source code by inserting counters at every branch, function, and line. When tests run, these counters record which code paths were executed, producing a coverage report. Gatekeeping: Set minimum coverage thresholds so CI fails if coverage drops. This prevents the slow erosion of test quality over time — without a gate, coverage tends to creep downward as deadline pressure mounts..nycrc or nyc.config.js.
7. Running in the Browser (Karma vs Headless)
Mocha runs in Node.js by default. To test frontend DOM logic:Option 1: JSDOM (Fast, Simulated)
Simulate browser in Node.Option 2: Karma (Real Browser)
Karma launches Chrome, injects Mocha + tests, reporting back to terminal. Complex setup, but 100% accurate.Option 3: Playwright/Puppeteer
Use Mocha just to drive Playwright automation (See Playwright course).8. Common Pitfalls & Debugging
The Arrow Function Context Trap
The Arrow Function Context Trap
TypeError: this.timeout is not a function or this.retries fails.
Cause: Arrow functions () => {} bind this lexically (from the parent scope), ignoring Mocha’s context.
Fix: Always use function() syntax when accessing this.Mixing 'done' and Promises
Mixing 'done' and Promises
Error: Resolution method is overspecified.
Cause: Your test accepts a done callback BUT also returns a Promise (async function). Mocha doesn’t know which one to listen to.
Fix: Use one or the other, never both.Global Variable Leaks
Global Variable Leaks
afterEach() to clean up state or Sinon Sandboxes to restore mocks.9. Interview Questions
Explain the difference between TDD, BDD, and Exports interfaces in Mocha.
Explain the difference between TDD, BDD, and Exports interfaces in Mocha.
- BDD:
describe(),it(),before(). Reads like behavior specs. (Most popular). - TDD:
suite(),test(),setup(). Closer to traditional unit testing. - Exports: Exporting an object where keys are descriptions. values are functions.
Mocha supports all of them via the
--uiflag (default isbdd).
Why do we need a separate assertion library like Chai?
Why do we need a separate assertion library like Chai?
2 + 2 = 4.
You can use Node’s built-in assert module, but Chai provides expressive, legible assertions (expect(x).to.be.true) that make failure messages easier to read.How does Mocha handle asynchronous code differently from synchronous code?
How does Mocha handle asynchronous code differently from synchronous code?
done), Mocha pauses execution until done() is called.
If a test function returns a Promise, Mocha waits for resolution/rejection.
If neither, it assumes synchronous execution and passes immediately if no error is thrown.10. Cheat Sheet
Interview Deep-Dive
Q: You are evaluating whether to use Mocha+Chai+Sinon or Jest for a new Node.js microservice. What factors drive your decision, and when would you pick Mocha?
Q: You are evaluating whether to use Mocha+Chai+Sinon or Jest for a new Node.js microservice. What factors drive your decision, and when would you pick Mocha?
- Pick Mocha when: You need fine-grained control over the test toolchain. For example, if the team has a custom assertion library, a specialized reporter for compliance auditing, or needs to run tests in a real browser via Karma. Mocha’s “bring your own everything” model means you are never fighting the framework’s opinions. I have also seen Mocha chosen in enterprise environments where the security team mandates auditing every dependency — Mocha’s smaller surface area (it is just a runner) means fewer transitive dependencies to vet.
- Pick Jest when: You want zero-config setup, built-in mocking, snapshot testing, and a single
package.jsondependency. For a team of 5 building a React app or a standard Node API, Jest’s batteries-included approach eliminates “which assertion library?” debates and gets tests running in minutes. - The hidden factor: Migration cost. If the codebase already has 2,000 Mocha tests with Sinon stubs, switching to Jest means rewriting every
sinon.stub()tojest.spyOn(), everyexpect(x).to.equal(y)toexpect(x).toBe(y), and dealing with subtle behavioral differences (Jest’s module mocking vs Sinon’s object-level stubs). That rewrite rarely delivers enough value to justify the risk.
clearMocks config that handles cleanup globally, Sinon requires explicit stub.restore() calls or sandbox usage. In a 500-test suite, a single missed restore() causes Test N+1 to see a stubbed method instead of the real one. The failure message gives no hint that a stub is involved — it just looks like the function returned the wrong value. The fix is mandatory sandbox usage in beforeEach/afterEach, but teams learn this the hard way after debugging a “phantom failure” for two hours.Q: Explain Mocha's two-phase execution model. A junior developer put database connection code inside a describe() block and tests are failing intermittently -- why?
Q: Explain Mocha's two-phase execution model. A junior developer put database connection code inside a describe() block and tests are failing intermittently -- why?
- Phase 1 (Parsing): When Mocha loads a test file, it executes the file top-to-bottom.
describe()callbacks run immediately — they are just function calls that register suites. Butit()callbacks are not executed; they are registered for later. This means any code insidedescribe()but outsideit(),before(), orbeforeEach()runs at parse time, potentially before any setup hooks. - Phase 2 (Execution): After all files are parsed, Mocha runs the registered tests in order, executing
before()hooks, thenit()blocks, thenafter()hooks.
const db = connectToDatabase() inside describe() runs during Phase 1. If the connection is async and returns a Promise, that Promise is never awaited — Mocha does not know about it. By the time it() blocks run in Phase 2, the connection might be established, partially open, or failed, depending on timing. This is why tests pass sometimes and fail other times.The fix is to move all setup into before() hooks: before(async () => { db = await connectToDatabase(); }). This runs during Phase 2, Mocha awaits the Promise, and tests only proceed once the connection is confirmed.- Broader lesson: Treat
describe()as a pure registration function. It should contain onlyit(),before(),after(), and nesteddescribe()calls. Any side effects (network calls, file I/O, global mutations) belong in hooks.
describe() runs at parse time, you can use loops or data arrays to generate it() blocks dynamically: testCases.forEach(tc => it('should...', ...)). The gotcha is that the data must be available synchronously at parse time. You cannot fetch test cases from an API and then generate tests from them — by the time the API responds, Mocha has already finished Phase 1 and will not register new tests. If you need dynamic test data, you must load it synchronously (e.g., require('./fixtures.json')) or use Mocha’s --delay flag, which defers Phase 2 until you explicitly call run().Q: Walk me through the difference between a Sinon spy, stub, and mock. A candidate who says 'they are all the same' is wrong -- why, and when do you reach for each?
Q: Walk me through the difference between a Sinon spy, stub, and mock. A candidate who says 'they are all the same' is wrong -- why, and when do you reach for each?
- Spy (observation only): Wraps a real function. The original implementation still executes. You use it when you want to verify a function was called without changing its behavior. Example: Spying on
console.logto verify your logger calls it with the right format, while still seeing the actual log output. The real function runs; you are just watching. - Stub (behavior replacement): Replaces a function entirely. The original implementation does not run. You use it to control return values, force errors, or simulate different behaviors across calls. Example: Stubbing
database.query()to return{ rows: [] }so your test does not need a real database. Stubs are the workhorse of unit testing. - Mock (expectation-driven verification): Combines observation with strict expectations. You declare upfront: “this method MUST be called exactly once with these arguments.” If the expectation is not met,
mock.verify()throws. Mocks are the strictest tool and the most controversial. Overusing them leads to “implementation tests” that break every time you refactor the code’s internal calling pattern, even if the external behavior is correct.
expect(stub.calledWith(...))) give you the same verification power without the rigidity.Follow-up: What is the “Sinon sandbox” pattern, and why is it non-negotiable for any serious test suite?A sandbox is a container that tracks every spy, stub, and mock you create. When you call sandbox.restore(), it restores all of them in one shot. Without sandboxes, you must manually call .restore() on every individual spy and stub in afterEach. Miss one, and the stub leaks into the next test. Sandboxes eliminate this entire class of bugs. The pattern is: create the sandbox in beforeEach, create all test doubles from the sandbox (sandbox.stub(...) instead of sinon.stub(...)), and restore in afterEach. This is the Sinon equivalent of Jest’s clearMocks: true config.Q: Why must you avoid arrow functions in Mocha test callbacks, and how does this relate to JavaScript's 'this' binding?
Q: Why must you avoid arrow functions in Mocha test callbacks, and how does this relate to JavaScript's 'this' binding?
this binding.- The mechanism: Mocha binds a context object to
thisinsidedescribe(),it(),before(), andafter()callbacks. This context object provides methods likethis.timeout(5000)andthis.retries(3). Arrow functions (() => {}) lexically capturethisfrom the enclosing scope — they ignore Mocha’s binding entirely. Sothis.timeout(5000)inside an arrow function refers to whateverthisis in the outer scope (oftenundefinedin strict mode ormodule.exportsin Node), not Mocha’s context. - The symptom:
TypeError: this.timeout is not a functionorthis.retries is not a function. - The fix: Use
function()syntax for any callback where you need Mocha’s context:it('test', function() { this.timeout(5000); }). - The nuance: If you do not use
this.timeout()orthis.retries()in a particular test, arrow functions are technically fine. But I recommendfunction()everywhere in Mocha for consistency, because debugging athisbinding issue in a 300-line test file is painful.
this binding at all — timeouts and retries are configured via jest.setTimeout() or config-level settings. Jest deliberately avoided the this pattern because arrow functions had become the community default by the time Jest gained popularity.Follow-up: Does this same arrow function issue apply to Sinon sandboxes or Chai assertions?No. Sinon and Chai do not use this binding in their APIs. sinon.createSandbox() returns a plain object, and Chai’s expect() is a function call, not a method on this. The arrow function issue is exclusively a Mocha concern because Mocha chose to put test configuration on the this context. This is one of the design decisions that makes Mocha feel dated compared to modern runners that use explicit function calls for configuration.