Back
Keentune
Testing curriculum 27 chapters
·
217 concepts
·
free
Everything the adaptive question bank can teach and test in Testing, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
Start practising Testing
New here? Read the Testing guide
A free 18-minute primer — the mental model, the mistakes beginners make, and what to practise first.
A. What a test is, and how a suite is built
•
a test names one behavior and fails loudly the moment it stops holding
•
set up state, perform exactly one action, assert the observable outcome
•
it is a pure alias of test ; the choice is readability, never semantics
•
describe groups tests and scopes hooks; it runs no assertions itself
•
two unrelated assertions in one test can't tell you which behavior broke
•
a test never observed red may be asserting nothing at all
•
*.test.js , *.spec.js and __tests__/ are discovered by default, via testMatch
•
a good failure names the expected value, the received value, and the subject
•
same input, same result: no dependence on the clock, the network, or test order
•
toBe uses Object.is , so two structurally equal objects are never toBe each other
•
toEqual compares recursively and ignores properties whose value is undefined
•
also checks undefined keys, array sparseness, and that the class matches
•
Object.is distinguishes +0 from -0 and treats NaN as equal to itself
•
toBe(0.3) fails on 0.1 + 0.2 ; toBeCloseTo takes a digit precision
•
toBeNull , toBeUndefined , toBeDefined , toBeTruthy , toBeFalsy and what each admits
•
identity membership versus deep-equality membership in a collection
•
toMatch takes a regex or a substring; toEqual on a string demands the whole value
•
pass a function, not its result, or the throw escapes before the matcher sees it
•
a string is a substring match, a regex a pattern, a class an instanceof check
•
.not inverts any matcher, and a negative assertion passes far too easily
C. The expect API in depth
•
expect.any , expect.anything and expect.stringContaining match *inside* a structure
•
assert a subset of an object's keys without pinning the fields you don't care about
•
assert membership without pinning order or length
•
expect.assertions(n) proves the async branch you meant to exercise actually ran
•
guards a test whose only assertion lives inside a callback that may never fire
•
.resolves /.rejects unwrap the promise before applying the matcher, and must be awaited
•
expect.extend returns {pass, message} ; the message is what makes the failure readable
•
a hard assertion aborts at the first failure, hiding the rest of the diff
•
a bare loop assertion hides which iteration failed unless the message carries the index
D. Setup, teardown and isolation
•
per-test setup versus once-per-file, and the shared-state risk beforeAll buys
•
outer beforeEach runs before inner; afterEach unwinds inside-out
•
cleanup in the test body is skipped when the test fails
•
a hook declared inside a describe wraps only that block's tests
•
a module-level object mutated by one test silently leaks into the next
•
before the test framework is installed versus after, and what each can touch
•
a hook that neither returns nor awaits its promise races the test it precedes
•
every test must pass in any order; randomizing order is how you expose hidden coupling
•
an unclosed server, socket or interval keeps the runner alive after the last test
•
a test that forgets to return or await passes before its assertion has run
•
an async test function is awaited by the runner, which is why await is enough
•
done must be called exactly once; calling it twice or never both fail the run
•
taking done *and* returning a promise is rejected by the runner
•
await expect(p).rejects.toThrow() asserts nothing if the await is missing
•
wrapping an assertion in try/catch can convert a real failure into a pass
•
an unawaited rejected promise surfaces later, failing an unrelated test
•
awaiting an already-resolved promise lets queued .then callbacks run before you assert
•
the default 5 s limit, and raising it as the third argument to test
•
a fixed sleep is either flaky or slow; poll for the condition instead
F. Test doubles — the taxonomy
•
a value passed only to satisfy a signature and never actually used
•
a stub answers a call with a fixed value; it makes no assertion
•
a spy observes the real behavior without replacing it
•
a mock asserts *how* it was called, which couples the test to the interaction
•
a simplified real implementation, e.g. an in-memory repository
•
assert the resulting state, or assert the calls made; they fail differently
•
mocking the unit under test means the test proves nothing about production code
•
replace I/O and third parties, never your own domain logic
•
the two schools disagree about collaborators, not about isolation
G. Mock functions and call inspection
•
jest.fn() records every call and returns undefined until told otherwise
•
mock.calls[i][j] is the j-th argument of the i-th call
•
mock.results records, per call, whether it returned or threw and with what
•
a queued ...Once value is consumed by one call, then the default takes over
•
replaces the body outright; mockImplementationOnce for a single call
•
sugar for an implementation returning a resolved promise, and its rejected twin
•
jest.spyOn wraps an existing method and by default still runs the original
•
clear the recorded calls, reset the implementation, or restore the original method
•
matches *some* call with deeply-equal arguments, which is not the same as the last call
H. Module mocking and dependency seams
•
jest.mock is hoisted above the imports, so its factory cannot close over a later variable
•
the second argument replaces the module wholesale, including its default export
•
automocking preserves the export shape and replaces every function with a mock
•
a __mocks__ file next to the module, or at the project root for a node_modules package
•
a package manual mock applies without jest.mock ; a user module still needs it
•
spread jest.requireActual to keep the untouched exports real
•
jest.resetModules makes the next require construct a fresh module instance
•
a destructured ESM import cannot be reassigned, so spy on the namespace or inject instead
•
passing the collaborator in removes the need for module surgery entirely
I. Fake timers and controlling the clock
•
installs a controllable clock in place of setTimeout , setInterval and Date
•
moves the fake clock forward and fires every callback that came due
•
drains the whole queue, and throws on an interval that reschedules forever
•
fires only what is already scheduled, so recursive re-scheduling cannot loop
•
advancing the clock does not run microtasks; you must await between steps
•
the ...Async advance methods yield to the microtask queue between callbacks
•
freezing the clock is what makes a date-dependent assertion deterministic
•
leaving fake timers installed hangs a later test that awaits real I/O
•
fake timers are how you assert a debounce or throttle without sleeping
•
a snapshot is a regression net; it never proves the output is right
•
the .snap file is committed and must be read in the diff like any other code
•
-u rewrites every snapshot, and blind updating turns the test into a rubber stamp
•
toMatchInlineSnapshot keeps the expected value next to the assertion
•
under --ci a missing snapshot fails instead of being silently written
•
deleting a test leaves a stale entry the runner reports but does not remove
•
expect.any(Date) inside a snapshot tolerates a field that legitimately changes
•
a thousand-line snapshot is unreviewable and passes by inertia
•
a serializer controls how a value prints, which is what keeps the diff legible
K. Runner configuration and environments
•
node versus jsdom , and the "document is not defined" error that names the wrong one
•
TypeScript/JSX source must be transformed before the runner can load it
•
node_modules is untransformed by default, which is why an ESM-only package explodes
•
maps path aliases, and stubs CSS or asset imports that the runner cannot parse
•
keeps build output and fixtures out of discovery
•
once per run in its own context versus once per test file in the test context
•
one command, several configurations, so node and jsdom suites share a run
•
no layout engine, so getBoundingClientRect returns zeros and nothing truly paints
•
testTimeout raises the default 5000 ms for every test rather than one at a time
L. The CLI, filtering and watch mode
•
-t filters by test name; a positional argument filters by file path
•
serial execution for debugging, or when tests share one external resource
•
worker count trades memory for wall time, and CI containers usually need it capped
•
test.only focuses a file's run, and a committed .only silently disables the rest
•
test.todo records intent without a body and reports separately from a skip
•
stops the run after the first failing suite, trading signal for minutes
•
--watch needs version control to compute which files changed
•
the flag that turns "Jest did not exit" into the name of the handle holding it
M. Coverage — the metrics and their limits
•
line, statement, branch and function coverage measure genuinely different things
•
branch coverage catches the untaken else that line coverage happily hides
•
code counts as covered by a test that asserts nothing about it
•
full coverage with weak oracles still ships defects
•
global and per-file thresholds fail the run when the number drops below the bar
•
hold the line on newly changed code instead of chasing a global percentage
•
source instrumentation versus the engine's own counters, and why the numbers disagree
•
mutate the source and see whether a test dies; it measures assertion strength, not reach
•
a surviving mutant that cannot change behavior, and why detecting them is undecidable
•
full branch coverage still exercises a tiny fraction of branch *combinations*
N. Vitest — the Vite-native runner
•
describe /it /expect match Jest, so the mental model transfers unchanged
•
helpers must be imported from vitest unless globals: true is set
•
vi.fn , vi.mock , vi.spyOn and vi.useFakeTimers mirror the jest object
•
tests run through the app's own Vite pipeline, so aliases and plugins already apply
•
no transpile-to-CommonJS step, which changes how and when module mocking can intercept
•
import.meta.vitest blocks live beside the code and are stripped from the build
•
several environments in one run, the Vitest analogue of Jest projects
•
it.concurrent overlaps tests within a file, making shared state a live hazard
•
a docblock comment overrides the environment for a single file
O. Testing Library — querying like a user
•
role first, then label, then text; a test id is the last resort
•
getBy* throws when nothing matches, so it doubles as an existence assertion
•
the only correct query when you are asserting that something is absent
•
findBy* returns a promise and retries until it matches or times out
•
getAllBy /queryAllBy return arrays; the singular form throws when two elements match
•
queries the accessible role and name, which is what assistive technology actually exposes
•
the name comes from aria-labelledby , then aria-label , then content
•
the query that fails when a form control has no real label, which is the point
•
matching trims and collapses whitespace by default, and exact controls case
•
a test id couples the test to markup no user can perceive
P. Async DOM testing and user interaction
•
the callback is retried until it stops throwing or the timeout expires
•
a waitFor body that never throws resolves on the first tick and asserts nothing
•
anything inside runs repeatedly, so side effects must stay out
•
asserts a disappearance, and fails if the element was never there to begin with
•
an update outside act means the assertion ran against a half-rendered tree
•
the DOM is unmounted between tests; without it, a query matches the previous test's render
•
userEvent dispatches the whole event sequence a real interaction produces
•
since user-event v14 every interaction returns a promise that must be awaited
•
reaching into state or props instead of rendered output tests the how, not the what
Q. Playwright locators and auto-waiting
•
a locator describes how to find an element and resolves at the moment of the action
•
before acting, Playwright waits for visible, stable, enabled and receives-events
•
role and label locators survive refactors; a CSS descendant chain does not
•
a locator resolving to two elements is an error, not a silent first-match
•
scope inside a parent, or filter({ hasText }) to disambiguate a repeated row
•
index selection bakes an ordering assumption into the test
•
text matching is substring and whitespace-normalized unless you ask for exact
•
locators pierce open shadow roots, but an iframe needs an explicit frame locator
•
a hard-coded wait is the single most common source of end-to-end flake
R. End-to-end assertions and fixtures
•
expect(locator).toBeVisible() retries; expect(await locator.isVisible()) does not
•
a retrying assertion carries its own timeout, separate from the test timeout
•
the full normalized text versus a substring, and which one over-specifies
•
pulling textContent first turns a retrying check into a one-shot race
•
test.extend hands each test a freshly built resource and disposes it afterwards
•
expensive setup shared across one worker's tests rather than the whole run
•
grouping actions into steps is what makes the trace and the report readable
•
expect.soft records the failure and lets the test continue collecting evidence
•
screenshot assertions need a per-platform baseline and an explicit pixel tolerance
S. Flakiness — causes and cures
•
a flaky test is one whose result changes with no change to the code under test
•
leaked state between tests is the most common cause; the fix is isolation, not a retry
•
real clocks, timezones and DST make an assertion that only fails on some days
•
asserting on the order of a collection whose order was never guaranteed
•
asserting mid-transition; disabling animations in test config removes the whole class
•
a real third-party request belongs in a contract or smoke test, never in a unit test
•
a retry can turn a genuine production race into a permanently green build
•
pull the flaky test out of the signal, but with a name and a deadline attached
•
repeating or stress-running a test is how you surface a one-in-fifty failure
•
two workers sharing a port, a temp file or a database row collide nondeterministically
T. Test strategy — what to test, and where
•
many fast unit tests, fewer integration tests, fewest end-to-end tests
•
for UI code, static analysis plus integration tests carry most of the confidence
•
a suite dominated by end-to-end tests is slow, flaky and expensive to diagnose
•
one unit, no I/O, measured in milliseconds, failing for exactly one reason
•
real collaborators across a boundary, such as code plus an actual database
•
a handful of revenue-critical journeys, not every field-level validation
•
consumer and provider verify a shared contract instead of standing up both systems
•
assert observable outcomes so an internal refactor cannot break the suite
•
a test is maintained forever, so a low-value test is a net liability
•
a fast post-deploy sanity check versus the suite that guards previously-fixed bugs
•
fail first, make it pass in the simplest way, then clean up under the net
•
code that is hard to test is almost always code that is badly coupled
•
implementing more than the current test demands throws away the feedback
•
a second example is what forces you to generalize instead of hard-coding
•
never change behavior and structure in the same step
•
start from the acceptance test and mock inward, or build the domain core first
•
"returns zero for an empty cart" beats "test cart" in every failure report
•
reproduce the defect as a failing test *before* fixing it, or the fix is unverified
•
pin the current behavior of legacy code before you are allowed to change it
V. Property-based, generative and fuzz testing
•
assert an invariant over generated inputs instead of one hand-picked case
•
a generator describes the input domain the runner is allowed to sample from
•
on failure the runner reduces the input to a minimal reproducible counterexample
•
decode(encode(x)) equals x is the easiest useful property to find in real code
•
compare the fast implementation against a slow, obviously-correct reference
•
something true of every output, such as a sort preserving length and multiset
•
record the seed, or a reported failure cannot be reproduced
•
feed hostile random input and assert only that nothing crashes or corrupts state
•
files are distributed across worker processes while tests within a file stay serial by default
•
split the suite across machines, then merge the shard reports into one result
•
each worker has its own process and module registry, so globals are not shared between them
•
--ci disables snapshot writing and interactive prompts so a run is reproducible
•
a machine-readable reporter feeds the CI UI while a human reporter serves triage
•
retain traces, videos and screenshots only for failures, or storage costs explode
•
a recorded timeline of DOM, network and actions is how you debug a failure you cannot reproduce
•
bailing early saves minutes but hides every failure after the first
•
run only the tests affected by the diff, with a full run gating the main branch
•
a fixture gives a test known prepared data or state, with setup kept reusable and explicit
•
test.each runs one assertion shape over a table of cases while reporting every case separately
Z. Systematic concurrency testing
•
control and bound scheduler choices so relevant thread interleavings can be explored and a failing schedule replayed
•
place synchronization at a suspected race window so the test proves both workers occupy the unsafe schedule
•
symbolic inputs explore feasible branch paths precisely, but the execution tree grows with the path space
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
Start practising Testing
All about Testing practice
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.
© 2026 SportaApp LLC