Testing cheat sheet and drills
The lookup file for chapters 16, 17 and 18, plus drills and flashcards. Written to be read side by side: if you know one framework, the parallel column tells you the other.
Table of contents
- 1. Side-by-side API reference
- 2. Assertion translation table
- 3. Mocking recipes
- 4. Command-line reference
- 5. Copy-paste skeletons
- 6. The testing trap list
- 7. Choosing a test double
- 8. Drills
- 9. Flashcards
- 10. Interview phrasebook
1. Side-by-side API reference
| Concept | node:test | unittest |
|---|---|---|
| Define a test | test('name', fn) / it('name', fn) | def test_name(self) in a TestCase |
| Group | describe('name', fn) | a TestCase subclass |
| Before all (group) | before(fn) | setUpClass(cls) (@classmethod) |
| After all (group) | after(fn) | tearDownClass(cls) |
| Before each | beforeEach(fn) | setUp(self) |
| After each | afterEach(fn) | tearDown(self) |
| Before all (module) | top-level code | setUpModule() |
| Guaranteed cleanup | t.after(fn) / scoped t.mock | self.addCleanup(fn, *args) (LIFO) |
| Context manager as fixture | try/finally, or using | self.enterContext(cm) (3.11+) |
| Skip | test.skip / { skip: true } / t.skip() | @unittest.skip / @skipIf / self.skipTest() |
| Known-broken | test.todo (exit 0 even if it throws) | @unittest.expectedFailure (fails if it passes) |
| Focus | test.only + --test-only | no built-in; use -k or a name |
| Sub-cases | await t.test('sub', fn) | with self.subTest(k=v): |
| Timeout | { timeout: 5000 } | none built-in (use --test-timeout equivalents in pytest) |
| Parallel | { concurrency: true }, --test-concurrency | none built-in (pytest-xdist) |
| Assertion count | t.plan(n) (counts t.assert.* only) | none |
| Poll until true | await t.waitFor(fn) | write a loop |
| Cancellation signal | t.signal | none |
| Snapshot | t.assert.snapshot(v) | none built-in |
| Coverage | --experimental-test-coverage | coverage run -m unittest |
| Doctests | none | doctest, wired in via load_tests |
| Async test | async () => {} | IsolatedAsyncioTestCase + async def test_x |
| Async fixtures | before(async () => {}) | asyncSetUp / asyncTearDown |
2. Assertion translation table
| Intent | node:assert/strict | unittest |
|---|---|---|
| Strict equality | assert.equal(a, b) | assertEqual(a, b) |
| Not equal | assert.notEqual(a, b) | assertNotEqual(a, b) |
| Deep equality | assert.deepEqual(a, b) | assertEqual(a, b) (type-dispatched) |
| Identity | assert.equal(a, b) for primitives; === | assertIs(a, b) |
| Is null/None | assert.equal(x, null) | assertIsNone(x) |
| Truthy | assert.ok(x) | assertTrue(x) |
| Membership | assert.ok(xs.includes(x)) | assertIn(x, xs) |
| Type | assert.ok(x instanceof C) | assertIsInstance(x, C) |
| Throws | assert.throws(fn, Err) | with assertRaises(Err): |
| Throws, message | assert.throws(fn, /re/) | with assertRaisesRegex(Err, r): |
| Rejects (async) | await assert.rejects(fn, Err) | with assertRaises(Err): inside async def |
| Regex on a string | assert.match(s, /re/) | assertRegex(s, r) |
| Float comparison | assert.ok(Math.abs(a-b) < eps) | assertAlmostEqual(a, b, places=/delta=) |
| Same elements, any order | assert.deepEqual([...a].sort(), [...b].sort()) | assertCountEqual(a, b) |
| Comparisons | assert.ok(a > b) | assertGreater(a, b) |
| Warnings | no built-in | with assertWarns(W): |
| Logs | spy on the logger | with assertLogs('x', level='INFO') as cap: |
| No logs | spy, assert not called | with assertNoLogs('x', level='WARNING'): |
| Partial object match | assert.partialDeepStrictEqual(a, b) (22.13+) | write a helper, or compare a subset |
| Unconditional fail | assert.fail('msg') | self.fail('msg') |
Two asymmetries worth remembering: Node has no warning or log assertions (spy on the logger), and
Python’s assertEqual dispatches on type so you get dict/list/set/multi-line-string diffs for free
where Node needs deepEqual.
3. Mocking recipes
| Recipe | node:test | unittest.mock |
|---|---|---|
| Spy | mock.fn() | Mock() |
| Stub returning a value | mock.fn(() => v) | Mock(return_value=v) |
| Stub raising | mock.fn(() => { throw e; }) | Mock(side_effect=e) |
| Async stub | mock.fn(async () => v) | AsyncMock(return_value=v) |
| Sequence of returns | fn.mock.mockImplementationOnce(...) per call | Mock(side_effect=[1, 2, 3]) |
| Computed return | mock.fn((a) => a * 2) | Mock(side_effect=lambda a: a*2) |
| Replace a method | mock.method(obj, 'm', impl) | patch.object(Cls, 'm', ...) |
| Spy but keep behaviour | mock.method(obj, 'm') (no impl) | Mock(wraps=real) |
| Replace a property/getter | mock.getter(obj, 'p', fn) | patch.object(C, 'p', new_callable=PropertyMock) |
| Replace a data field | mock.property(obj, 'k', v) | patch.object(obj, 'k', v) |
| Patch a module member | mock.module(path, { namedExports }) + flag | patch('consumer.name') |
| Patch a dict / env | assign and restore manually | patch.dict(os.environ, {...}) |
| Signature-checked double | none built-in | create_autospec(C, instance=True) / patch(..., autospec=True) |
| Freeze/advance time | t.mock.timers.enable/tick/setTime/runAll | inject a clock (no built-in) |
| Mock file I/O | mock.module('node:fs', ...) | patch('builtins.open', mock_open(read_data=...)) |
| Assert called | assert.ok(fn.mock.callCount() > 0) | m.assert_called() |
| Assert called once with | assert.deepEqual(fn.mock.calls[0].arguments, [a]) + count | m.assert_called_once_with(a) |
| Assert not called | assert.equal(fn.mock.callCount(), 0) | m.assert_not_called() |
| Assert call order | compare fn.mock.calls.map(c => c.arguments) | m.assert_has_calls([call(1), call(2)]) |
| Assert awaited | n/a (a promise is a value) | m.assert_awaited_once_with(a) |
| Match any argument | write a predicate | ANY |
| Opaque placeholder value | Symbol('reqId') | sentinel.req_id |
| Reset history | fn.mock.resetCalls() | m.reset_mock() |
| Restore everything | mock.restoreAll() (or use t.mock) | patch.stopall() (or addCleanup) |
The two most important rows
autospec has no Node equivalent. Python’s create_autospec checks method names and signatures
and produces AsyncMock for async def. In node:test the equivalent safety comes from TypeScript: a
typed mock.fn<(m: Money) => string>() is checked at compile time. So: use autospec=True in Python,
and use types in TypeScript.
mock.timers has no Python equivalent. Python has no built-in clock mocking (freezegun is the
third-party answer). Both languages agree on the better move anyway: inject the clock.
4. Command-line reference
# ---------- node:test ----------
node --test # discover and run
node --test --watch # rerun on change
node --test --test-reporter=spec # human output (default from Node 23; TAP before)
node --test --test-name-pattern='places an order'
node --test --test-skip-pattern='integration'
node --test --test-only # honour `only`
node --test --test-concurrency=4
node --test --test-timeout=5000
node --test --test-shard=1/3 # splits FILES, not tests
node --test --experimental-test-coverage
node --test --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-lines=85
node --test --test-reporter=junit --test-reporter-destination=results.xml
node --test --test-update-snapshots
node --test --experimental-test-module-mocks # required for mock.module
node --test file.test.ts # TS: type stripping is default-on from 22.6
node --experimental-transform-types --test x.ts # needed for enum/namespace/param properties
# ---------- unittest ----------
python -m unittest # discover from cwd
python -m unittest discover -s src -p 'test_*.py' -t .
python -m unittest -v # one line per test
python -m unittest test_mod.TestClass.test_method
python -m unittest discover -k autospec # substring/glob filter
python -m unittest --failfast
python -m unittest --buffer # hide stdout for passing tests
python -m unittest --locals # locals in tracebacks
python -m unittest --durations 5 # slowest 5 (3.12+)
python -m doctest module.py -v
coverage run -m unittest && coverage report -m && coverage html
5. Copy-paste skeletons
node:test
import { test, describe, it, before, after, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert/strict';
import { OrderService } from '../src/order-service.mjs';
function makeService(overrides = {}) {
const repo = { findBySku: mock.fn(async () => ({ sku: 'ABC', price: 250, stock: 4 })),
decrement: mock.fn(async () => {}) };
const payments = { charge: mock.fn(async () => ({ id: 'rcpt_1' })) };
const deps = { repo, payments, clock: () => 1_700_000_000_000, ...overrides };
return { svc: new OrderService(deps), ...deps };
}
describe('OrderService.place', () => {
it('charges the computed total and decrements stock', async () => {
const { svc, repo, payments } = makeService();
const order = await svc.place({ sku: 'ABC', qty: 2, card: 'tok' });
assert.equal(order.total, 500);
assert.deepEqual(payments.charge.mock.calls[0].arguments, [{ card: 'tok', amount: 500 }]);
assert.deepEqual(repo.decrement.mock.calls[0].arguments, ['ABC', 2]);
});
it('does not charge when out of stock', async () => {
const { svc, payments } = makeService({
repo: { findBySku: mock.fn(async () => null), decrement: mock.fn() } });
await assert.rejects(() => svc.place({ sku: 'X', qty: 1, card: 't' }),
{ name: 'OutOfStockError' });
assert.equal(payments.charge.mock.callCount(), 0);
});
});
Integration skeleton (node:test)
import http from 'node:http';
import { once } from 'node:events';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
let server, base, dir, db;
before(async () => {
server = http.createServer(app);
server.listen(0); // ephemeral port
await once(server, 'listening');
base = `http://127.0.0.1:${server.address().port}`;
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-'));
const { DatabaseSync } = await import('node:sqlite');
db = new DatabaseSync(':memory:');
db.exec(schemaSql);
});
after(async () => {
server.close(); await once(server, 'close');
await fs.rm(dir, { recursive: true, force: true });
db.close();
});
unittest
import unittest
from unittest.mock import create_autospec, patch, call
class TestOrderService(unittest.TestCase):
class Repo:
def find_by_sku(self, sku): ...
def decrement(self, sku, qty): ...
def setUp(self):
self.repo = create_autospec(self.Repo, instance=True) # names AND signatures
self.repo.find_by_sku.return_value = {"price": 250, "stock": 4}
self.charge = self.enterContext( # 3.11+, auto-cleaned
patch("shop.service.charge", autospec=True))
self.charge.return_value = {"id": "rcpt_1"}
self.svc = OrderService(repo=self.repo, clock=lambda: 1_700_000_000)
def test_charges_the_computed_total(self):
order = self.svc.place("ABC", 2, "tok")
self.assertEqual(order["total"], 500)
self.charge.assert_called_once_with("tok", 500)
self.repo.decrement.assert_called_once_with("ABC", 2)
def test_does_not_charge_when_out_of_stock(self):
self.repo.find_by_sku.return_value = None
with self.assertRaises(OutOfStockError):
self.svc.place("X", 1, "tok")
self.charge.assert_not_called()
self.repo.decrement.assert_not_called()
def test_validation_runs_before_any_dependency(self):
for bad in (0, -1, 2.5, True, "3"):
with self.subTest(qty=bad):
with self.assertRaises(ValueError):
self.svc.place("ABC", bad, "tok")
self.repo.find_by_sku.assert_not_called()
Integration skeleton (unittest)
import sqlite3, tempfile, threading, http.server, urllib.request
from pathlib import Path
class TestIntegration(unittest.TestCase):
def setUp(self):
self.dir = Path(self.enterContext(tempfile.TemporaryDirectory()))
self.db = sqlite3.connect(":memory:")
self.db.row_factory = sqlite3.Row
self.db.execute("PRAGMA foreign_keys = ON") # OFF by default!
self.db.executescript(SCHEMA)
self.addCleanup(self.db.close)
@classmethod
def setUpClass(cls):
cls.server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler) # port 0
cls.port = cls.server.server_address[1]
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
cls.thread.start()
@classmethod
def tearDownClass(cls):
cls.server.shutdown(); cls.server.server_close(); cls.thread.join(timeout=2)
Async skeleton (unittest)
class TestAsync(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.repo = AsyncMock()
self.repo.find_by_sku.return_value = {"price": 100}
self.sleeps = []
self.svc = Service(self.repo, sleep=AsyncMock(side_effect=self.sleeps.append))
async def test_retry_backoff(self):
self.repo.find_by_sku.side_effect = [RuntimeError, RuntimeError, {"price": 50}]
await self.svc.with_retry("ABC")
self.assertEqual(self.sleeps, [0.1, 0.2])
self.repo.find_by_sku.assert_awaited()
6. The testing trap list
Everything here was hit or verified while writing chapters 16-18.
Both languages
- Coverage is not verification. Two suites, identical 100% line coverage, mutation scores 0/5 and 4/5.
- A test that cannot fail is decoration. Ask “if I broke the code, would this fail?” before committing.
assert_called_with/ last-call assertions check only the most recent call. Useonce_withwhen you mean once.- Hard-coded ports break under parallelism. Bind to port 0.
- Shared mutable fixtures create order-dependent tests. Use a factory function, not a shared object.
- Real
sleepin tests is the top cause of both slowness and flakiness. Inject the clock. - Unseeded randomness makes failures unreproducible. Seed it.
- Set/dict-view iteration order is not a stable assertion target. Sort, or use
assertCountEqual/ a sorted comparison. - Float equality.
0.1 + 0.2 != 0.3. - Mocking what you should integrate with. A mocked repository accepts a duplicate primary key, a negative price, and a dangling foreign key. The real engine refuses all three.
- Asserting on call sequences couples the test to the implementation; the refactor breaks it with no bug introduced.
node:test specifics
t.plan(n)countst.assert.*and subtests only — bareassertcalls are invisible to it.- Missing
awaitonassert.rejectsmakes the test pass unconditionally. Nothing warns you. deepStrictEqualcompares prototypes, so a null-prototype object (fromnode:sqlite,Object.create(null), a worker boundary) fails against a plain literal with a diff that looks identical. Spread it.- A failing
todoprints a stack trace but exits 0. Read the tally line, not the colour. mock.calls[i].targetisundefinedfor a plain call and a function fornew— not reference-equal to your mock.mock.modulerequires the flag and depends on import order. Statically imported modules were already resolved; useawait import()after installing the mock.- Type stripping is not compiling.
enumfails withERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. Use--experimental-transform-types, a loader, orerasableSyntaxOnly. - Neither stripping nor a loader type-checks your tests.
tsc --noEmitis a separate CI step. - Default reporter is TAP on Node 22 and earlier,
specfrom 23. - The global
mocktracker needs manual restore. Prefert.mock.
unittest specifics
- Patch where the name is looked up, not where it is defined.
from x import ybindsyin the importing module. - Misspelled
assert_*methods raise; misspelled ordinary method names do not.m.find_by_skew(1)is silently fine — that is whatautospecprevents. specchecks names but not signatures. Onlyautospecchecks arity.create_autospecdoes not accept the**{"child.return_value": v}kwargs form. Configure children after construction.patch.multipleyields only the mocks it created. PassDEFAULTif you want it handed to you.- A plain
Mockis not awaitable —TypeError: object Mock can't be used in 'await' expression. UseAsyncMock, orcreate_autospec, which gets it right automatically. - Called and awaited are different events.
call_count == 1withawait_count == 0is a forgottenawait. PRAGMA foreign_keys = ON— SQLite ignores foreign keys otherwise.- Permission tests do not work as root, which is the default in most containers. Guard with
os.geteuid(). assertTrue(a == b)throws away the type-dispatched diff. UseassertEqual.- Discovery needs importable packages. A missing
__init__.pyyields “0 tests”. - An
@expectedFailuretest that passes fails the run — deliberately, so a fixed bug cannot stay marked broken.
7. Choosing a test double
graph TD
A["Does the collaborator get called<br/>on this path?"] -->|no| B["DUMMY<br/>(make it raise, so you find out if that changes)"]
A -->|yes| C{"Do you only need it<br/>to return something?"}
C -->|yes| D["STUB"]
C -->|no| E{"Is the CALL itself the<br/>behaviour you must verify?"}
E -->|yes| F["SPY (assert after) or<br/>MOCK (expectations first)"]
E -->|"no - state you can inspect"| G{"Do you own it,<br/>and does it have state?"}
G -->|yes| H["FAKE<br/>(+ a contract test)"]
G -->|no| I{"Is it infrastructure?"}
I -->|yes| J["REAL thing<br/>(sqlite :memory:, port 0, mkdtemp)"]
Does the collaborator get called on this path?
├── no ──> DUMMY (make it raise, so you find out if that changes)
└── yes
├── Do you only need it to return something?
│ └── yes ──> STUB
└── Is the CALL itself the behaviour you must verify?
├── yes ──> SPY (assert after) or MOCK (expectations first)
└── no — there is state you can inspect
├── Do you own it, and does it have state? ──> FAKE (+ a contract test)
└── Is it infrastructure? ──> use the REAL thing
(sqlite :memory:, port 0, mkdtemp)
| I need to… | Reach for |
|---|---|
| test pure logic | nothing — call the function with values |
| test a branch that never reaches a dependency | a dummy that raises |
| supply an input | a stub |
| prove something was not done | a spy plus a not_called assertion |
| prove ordering (“validated before querying”) | a spy plus not_called on the later dependency |
| test stateful rules (duplicates, balances, quotas) | a fake, plus a contract test against the real one |
| test a constraint the database enforces | the real database (sqlite3 / node:sqlite, in memory) |
| test serialization, status codes, headers | a real local server on port 0 |
| test file handling | mkdtemp / TemporaryDirectory |
| test time-dependent behaviour | an injected clock |
| test backoff durations | an injected sleep spy, then assert the durations |
| test a third-party API | a fake at your own boundary + one contract test against the real service |
| stop a refactor from breaking tests | assert on state, not on call sequences |
8. Drills
Timed, from a blank file, reference closed. Same rules as Problem sets §6: write a test, run it, then diff against the chapter.
| # | Drill | Target | Reference |
|---|---|---|---|
| 1 | A node:test file with describe/it, all four hooks, and one assert.rejects | 10 min | 16 §3 |
| 2 | The same in unittest, with setUpClass, addCleanup and assertRaisesRegex | 10 min | 17 §1 |
| 3 | mock.fn spy: assert call count, arguments, and a thrown error | 10 min | 16 §6 |
| 4 | Mock with all four side_effect modes | 15 min | 17 §6 |
| 5 | Patch a function in the module that uses it, and prove patching the definition site does nothing | 15 min | 17 §9 |
| 6 | create_autospec: show it catching a wrong method name and a wrong arity | 10 min | 17 §10 |
| 7 | Test exponential backoff two ways: mock.timers and an injected sleep | 20 min | 16 §8 |
| 8 | AsyncMock: assert awaited-with, and demonstrate the called-but-not-awaited case | 15 min | 17 §12 |
| 9 | A real HTTP server on port 0, asserting a 201, a 400, and a timeout | 25 min | 16 §15 |
| 10 | In-memory SQLite: assert a PRIMARY KEY, a CHECK and a FOREIGN KEY violation | 20 min | 17 §14 |
| 11 | A TemporaryDirectory fixture asserting a round trip and a real ENOENT | 10 min | 17 §14 |
| 12 | All five test doubles for the same collaborator | 25 min | 18 §2 |
| 13 | A contract test suite that a fake and a real implementation both pass | 20 min | 18 §3 |
| 14 | Split a function into a pure core and a thin shell; table-test the core | 25 min | 18 §7 |
| 15 | Write a weak suite with 100% coverage, then mutate the code and show it survives | 30 min | 18 §9 |
| 16 | Take an untestable function (hard-coded clock, random, env) and add three seams | 20 min | 18 §6 |
| 17 | subTest / table-driven tests over a boundary table (>= 5, >= 10) | 10 min | 17 §4 |
| 18 | A snapshot test, then normalize a timestamp and an ID out of it | 15 min | 16 §10 |
| 19 | Run coverage with a threshold and explain why funcs% < lines% | 10 min | 16 §11 |
| 20 | A characterization test for a function whose behaviour you do not know | 15 min | 18 §16 |
Starred for the weekly rotation in Study plan §4: 3, 5, 6, 7, 9, 10, 12, 14.
Debug-and-fix drills
Write the bug, then find it a day later. Each is a real failure mode from the trap list.
- A test with
assert.rejectsand noawait. Prove it passes when the function resolves. - A
t.plan(2)test with two bareassertcalls. Explain the failure message. - A
deepStrictEqualagainst anode:sqliterow. Explain the identical-looking diff. - A patch applied to the definition site instead of the usage site. Show the real function still runs.
- A
Mock()where anAsyncMock()is needed. Read theTypeError. - A suite that passes alone and fails in a group because of a forgotten
mock.restoreAll(). - Two tests sharing a module-level list. Make one pass alone and fail after the other.
- A test asserting
list(some_set) == [...]. Run it until it fails. - An integration test on a hard-coded port. Run two copies at once.
- A SQLite integration test missing
PRAGMA foreign_keys = ONthat fails to catch a dangling reference.
9. Flashcards
Say the answer out loud before reading it.
What is the difference between a stub and a mock? A stub supplies canned answers and verifies nothing. A mock has expectations declared up front and verifies them. The deeper distinction is state verification versus interaction verification.
When should you build a fake instead of a stub? When the collaborator has state and you own it. A stub that forgets makes stateful rules (duplicates, balances, quotas) untestable while looking tested.
What obligation does a fake create, and how do you discharge it?
It can drift from the real implementation. Write a contract test suite — a mixin of behaviour tests
inherited by one TestCase per implementation — that both must pass.
Is 100% coverage a good goal? No. Two suites with identical 100% line coverage scored 0/5 and 4/5 on mutation testing. Coverage measures execution, not verification. Use it as a floor and a ratchet on changed lines.
What is mutation testing?
Introduce small code changes and check whether the suite fails. Surviving mutants are covered but
unverified code. Some mutants are equivalent (x < lo vs x <= lo when both return the same value)
and cannot be killed, so scores are never 100%.
What is the single best question to ask about a test? “If I broke the code, would this test fail?”
Where do you patch, and why?
Where the name is looked up, not where it is defined. from x import y copies the reference into the
importing module at import time, so rebinding x.y afterwards does nothing.
What does autospec=True buy you over spec=?
spec checks attribute names; autospec checks names and signatures, recursively, and produces
AsyncMock for async def members.
Which misspellings does unittest.mock catch?
Names starting with assert/assret raise AttributeError. Ordinary method names do not —
m.find_by_skew(1) is silently fine. That gap is what autospec closes.
Name the four modes of side_effect.
An exception (raised), an iterable (one per call then StopIteration), a callable (its return value is
used), and a callable returning DEFAULT (falls back to return_value).
What does t.plan(n) count in node:test?
t.assert.* calls and subtests. Bare node:assert calls are invisible to it.
Why is assert.rejects without await dangerous?
It returns a promise; without await the assertion never runs and the test passes even when the function
resolves. Nothing warns you — a no-floating-promises lint rule is the only defence.
Why did deepStrictEqual fail on two objects that print identically?
It compares prototypes. A null-prototype object — from node:sqlite, Object.create(null), or a worker
boundary — is not deep-strict-equal to a plain object literal.
What is the difference between test.todo and @expectedFailure?
Both tolerate failure. @expectedFailure additionally fails the run if the test passes, so a fixed
bug cannot stay marked broken. test.todo does not — it prints a stack trace and exits 0.
How do you test time-dependent behaviour?
Inject the clock and advance it. mock.timers works in Node and has a much larger blast radius; Python
has no built-in equivalent. Injecting is also strictly stronger for backoff, because you can assert the
durations.
How do you test exponential backoff?
Inject the sleep function as a spy and assert the sequence of durations ([100, 200]) plus the attempt
count. Microseconds, and it asserts the actual requirement.
How do you avoid port conflicts in integration tests? Bind to port 0 and read back the assigned port. Never hard-code.
What can an in-memory SQLite integration test catch that a mocked repository cannot?
Real constraint enforcement — PRIMARY KEY, CHECK, FOREIGN KEY — plus transactions, SQL syntax and type
coercion. Remember PRAGMA foreign_keys = ON; SQLite ignores them otherwise.
Called versus awaited on an AsyncMock?
Separate events. call_count == 1 with await_count == 0 means a coroutine was created and never
awaited.
What happens if you await a plain Mock()?
TypeError: object Mock can't be used in 'await' expression. Use AsyncMock, or create_autospec,
which detects coroutine functions automatically.
What is subTest for?
Reporting every failing case in a table instead of stopping at the first, each labelled with the keyword
arguments you pass. The cost is that cases are not individually selectable.
What are the five common causes of flaky tests? Real time, unseeded randomness, iteration order (sets, filesystem listings), shared mutable state between tests, and float comparison. Plus hard-coded ports and unawaited promises.
Should you auto-retry a flaky test? No — quarantine it. A retry hides the signal while the race stays in production code, where nothing retries. Retry only at the end-to-end level against genuinely unreliable infrastructure, and log it.
What is a characterization test? A test that records what the code currently does, bugs included, so a refactor’s behaviour changes show up as diffs. It asserts sameness, not correctness.
What is “functional core, imperative shell” worth in testing terms? The branch matrix moves into pure functions that need no doubles at all, and the mocked tests shrink to one thin test per I/O path.
Pyramid or trophy? Depends where the complexity is. A library with real algorithms is pyramid-shaped; a service moving JSON between HTTP and SQL is trophy-shaped because the risk is all in the wiring. The ice cream cone is always wrong.
What is contract testing? The consumer records what it needs; the provider’s CI verifies it can deliver that, without the two ever running together. The cheap version is a shared schema validated on both sides.
Which properties are worth reaching for in property-based testing?
Round trip (decode(encode(x)) == x) first — it finds most serialization bugs — then invariants,
idempotence, commutativity, and oracle comparison against a slow reference implementation.
Why does type stripping fail on enum in a .ts test file?
Stripping erases types; enum has runtime semantics. Use --experimental-transform-types, a loader, or
erasableSyntaxOnly in tsconfig.json so you never write non-erasable syntax.
Do node --test file.ts or a loader type-check your tests?
No. Both discard types without validating them. tsc --noEmit is a separate step.
What should you test at the integration level rather than the unit level? Anything where a mock would encode your belief about a boundary: SQL, migrations, serialization, HTTP status codes, and constraints the database enforces.
When are interaction assertions the right choice? When the interaction is the observable behaviour: “did not charge the card”, “validated before querying”, fire-and-forget side effects (email, metric, audit line), and third-party boundaries you must not cross.
10. Interview phrasebook
On strategy
- “I’d unit test the decision logic exhaustively and integration test the boundaries — a mocked repository only encodes my belief about the schema, so the constraint bugs live where the mock is.”
- “The shape of the suite follows where the complexity is. This service is mostly wiring, so I’d weight toward integration tests; a pricing engine I’d weight toward units.”
- “I’d assert on state rather than call sequences, so a refactor that preserves behaviour keeps the tests green.”
On doubles
- “That collaborator has state, so I’d build a fake rather than a stub — and a contract test suite so the fake can’t drift from the real one.”
- “This one’s a negative assertion: the important test is that the card was not charged, and there’s no state that proves it, so a spy is right.”
On coverage and quality
- “Coverage tells me what ran, not what was verified. I’d use it as a ratchet on changed lines rather than an absolute target.”
- “If I want to know whether the assertions bite, that’s mutation testing — change the code slightly and see whether the suite notices.”
- “Before I commit a test I ask whether it would fail if I broke the code. If I can’t answer immediately, it isn’t earning its place.”
On the hard cases
- “I’d inject the clock rather than freeze time globally — smaller blast radius, and it makes the backoff durations assertable instead of just the fact that it waited.”
- “Port 0, so the OS picks the port. That removes the whole class of failures where the suite passes alone and fails in parallel.”
- “In-memory SQLite gives me real constraint enforcement with no container and no cleanup.”
On flakiness
- “I’d treat it as a bug in the test and quarantine it rather than retry it — a retry hides the signal while the race stays in production code.”
- “The usual causes are real time, unseeded randomness, iteration order, and shared state between tests. Each has a deterministic fix.”
On legacy code
- “Characterization test first to pin the current behaviour, then find a seam, then refactor behind the net. Never a rewrite with the tests written afterwards.”
- “For coverage policy on legacy code I’d require tests on changed lines rather than on the file — a ratchet that actually turns.”
Back to the index, or the tool chapters: node:test · unittest · strategy.