Exploratory Testing — Finding the Bugs Scripts Miss
Every page in this Part so far has, at its heart, a script. Functional tests check pre-written expectations. The regression suite re-runs known checks forever. Even performance and security testing largely execute plans you designed in advance. That is their strength: a script proves a known expectation, repeatedly and cheaply.
But a script can only ever find the bug it was written to look for. The recurring question of this whole book is how do you think about the edge cases that break it? — and here is the uncomfortable answer: the worst edge cases are the ones nobody thought to write a test case for. You cannot script a test for a bug you don’t yet know exists. This page is about the discipline that finds those: exploratory testing.
What exploratory testing is
Section titled “What exploratory testing is”Exploratory testing is testing where the tester simultaneously designs and runs tests, letting each result steer the next probe. There is no pre-written script. You interact with the software, observe what it actually does, form a hypothesis about where it might be weak, and immediately act on that hypothesis — and the outcome of that action tells you where to look next.
The classic definition, from Cem Kaner who coined the term in 1984, is: simultaneous learning, test design, and test execution. Those three activities are collapsed into one tight loop instead of being separate, sequential phases:
scripted testing (three phases, in order): design tests ──► execute tests ──► learn from results (done once, up front) (mechanically) (later, if ever)
exploratory testing (one loop, all at once): ┌─────────────────────────────────────┐ ▼ │ observe ──► hypothesize ──► probe ──► learn "what does "what might "try it" "now I know it do?" break it?" more — probe somewhere new"The key word is steer. If you type a 500-character name into a form and the page glitches, you don’t move on — you follow it: try 5,000 characters, then an emoji, then a name that is all spaces. Each result redirects the next probe. That steering is what makes it neither scripted nor random.
It is worth killing the biggest misconception immediately: exploratory testing is not ad-hoc button-mashing. Random clicking has no hypothesis and no memory; it stumbles into bugs by luck. Exploratory testing is directed by a skilled tester’s model of how the system is built and where systems like it tend to fail. The randomness is in the inputs, never in the thinking.
Scripted vs exploratory — checking vs discovering
Section titled “Scripted vs exploratory — checking vs discovering”The sharpest way to hold the two apart is a distinction the testing community draws between checking and exploring:
- Checking confirms things you already believe. You had an expectation; the check tells you whether it still holds. This is the domain of the regression suite — it protects known truths against change.
- Exploring discovers things you did not know. You had no expectation; the exploration surfaces a surprise. This is where genuinely new bugs are born.
Put another way, in the language of known and unknown:
│ we KNOW to look for it │ we DON'T know to look for it ──────────────┼────────────────────────┼────────────────────────────── scripted / │ known knowns │ ✗ blind spot checking │ "does login work?" │ (a script can't ask an │ → automated regression │ unasked question) ──────────────┼────────────────────────┼────────────────────────────── exploratory / │ known unknowns │ UNKNOWN UNKNOWNS exploring │ "let me poke the │ "...huh, THAT wasn't │ date parser" │ supposed to happen"The bottom-right cell is the whole point. Scripted testing structurally cannot reach it — a test case is a question written in advance, and you cannot write the question for a surprise you haven’t had yet. Exploratory testing is the only technique aimed directly at unknown unknowns: the surprising bug that no one anticipated because if they had anticipated it, they’d have written a test and fixed it already.
This is not a competition. Scripted checking and exploratory exploring answer different questions and belong together. The mistake is believing that a green regression suite means the software is correct — it means the software still does the specific things you already knew to check. Everything you didn’t know to check is untouched, and exploration is how you find it.
Structure — how it stays disciplined, not aimless
Section titled “Structure — how it stays disciplined, not aimless”The obvious objection to “no script” is: how do you know you covered anything? How do you avoid wandering in circles? Good exploratory testing is not freeform wandering; it is structured by three lightweight tools, usually grouped under the name session-based test management (SBTM), introduced by Jonathan and James Bach around 2000.
Charters — a mission, not a script
Section titled “Charters — a mission, not a script”A charter is a one-sentence mission for a session: what to explore and why, without dictating the steps. It gives direction while leaving all the tactical decisions to the tester in the moment.
Charter: Explore the CSV import feature with malformed and oversized files to discover how it handles partial and corrupt input.A charter is deliberately not a set of steps. “Upload a 2 GB file, then upload a file with a missing column, then…” would be a script. The charter names the territory; the tester chooses the route live, based on what each upload reveals.
Time-boxed sessions — a fixed budget of focus
Section titled “Time-boxed sessions — a fixed budget of focus”A session is an uninterrupted, time-boxed block — typically 60 to 120 minutes — spent working a single charter. The time-box matters for two reasons. It forces focus (one charter, no drifting), and it makes the invisible measurable: instead of “we did some exploratory testing,” you can say “four sessions against the import feature,” which is something a team can plan, review, and repeat.
Note-taking — making the invisible auditable
Section titled “Note-taking — making the invisible auditable”Because there is no script to point back at, the tester takes notes as they go: what was tried, what happened, what looked suspicious, what to follow up on. Those notes are the deliverable. They turn a session from an untraceable “trust me, I looked” into a reviewable record — and, crucially, into the raw material for new automated checks (more on that below).
SESSION SHEET Charter : explore CSV import w/ malformed + oversized files Tester : A. Ng Duration : 90 min --------------------------------------------------------------- TESTED - 2 GB file → OK, streamed, no OOM - missing column → clear error, row rejected ✓ - column w/ 10k chars → ⚠ truncated silently, NO warning BUGS - #1 duplicate header row → 2nd row imported as DATA (bad) - #2 trailing comma → phantom empty column, crashes on save FOLLOW-UP - try UTF-8 BOM prefix? embedded newline in quoted field?Charter, time-box, notes. That is the entire scaffolding — light enough not to smother the improvisation, firm enough to make it accountable.
The mindset — where the edge cases actually get found
Section titled “The mindset — where the edge cases actually get found”Here is the payoff, and it is the reason this page exists in a book obsessed with the edge cases that break it. The nastiest edge cases are found by a skeptical, curious human deliberately trying to break the thing — not by a suite dutifully confirming what it was told to confirm.
An exploratory tester carries a mental library of “places software tends to lie”: the empty input, the enormous input, the input with a leading zero, the name with an apostrophe (O'Brien), the emoji in the username, the two operations racing each other, the timezone that has no such hour because of daylight saving, the moment a counter rolls over. Scripts rarely ask these questions because the person who wrote the feature — and the person who wrote its tests — shared the same blind spots. A different mind, actively hunting, is what pierces them.
Consider the kvlite key-value store in this repo. Its scripted tests (rust/kvlite/tests/server.rs) prove the happy path: a client can SET, GET, and DEL a normal key over TCP. An exploratory session against the same server asks the questions the script never thought to: What if the key is empty? What if the value is a megabyte? What if I send half a command and then hang up? What if two clients SET the same key in the same millisecond (rust/kvlite/tests/concurrency.rs exists precisely because someone once asked that question exploratorily)? Every one of those is a potential edge case — and every one was found by a person poking, not by a test case that already knew the answer.
When it shines — and its hard limit
Section titled “When it shines — and its hard limit”Exploratory testing is irreplaceable for discovery. When a feature is new, when the requirements are fuzzy, when you’re learning a system you didn’t build, when you suspect there are bugs but can’t name them — nothing beats a skilled human exploring. It excels exactly where scripts are useless: in the space of the not-yet-imagined.
But it has one defining limitation, and it is the mirror image of its strength: exploratory testing is not repeatable. A session is a performance — it happened once, driven by one tester’s intuition on one afternoon. You cannot run it again on tomorrow’s build and get the same coverage, and you cannot put it in a CI pipeline as a gate. The very freedom that lets it find unknown unknowns also means it protects nothing automatically.
The resolution is not to choose between exploratory and scripted testing — it is to connect them into a cycle:
┌──────────────────┐ finds a bug ┌────────────────────┐ │ EXPLORATORY │ ─────────────────────────► │ write a scripted │ │ (discovery: │ (the unknown unknown │ test that pins │ │ unknown │ becomes known) │ the now-known bug │ │ unknowns) │ └─────────┬──────────┘ └──────────────────┘ │ add to ▲ ▼ │ freed to hunt NEW surprises ┌────────────────────┐ └────────────────────────────────────── │ AUTOMATED │ (regression now guards this bug │ REGRESSION SUITE │ forever, so you never re-find it) │ (guards it forever)│ └────────────────────┘This is the single most important operational habit around exploratory testing: every bug it finds should be fed back into the automated regression suite as a new scripted test. The exploratory session discovers the unknown unknown once; the regression test it spawns makes sure that bug can never silently return. Exploration converts unknowns into knowns; automation then guards the knowns cheaply forever — which frees the next exploratory session to go hunt new unknowns instead of re-checking old ground. The two techniques are not rivals; they are a pump that steadily drains the swamp of undiscovered bugs.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because a test case is a question written in advance, and the most dangerous bugs are answers to questions nobody thought to ask. Exploratory testing exists to reach the unknown unknowns that scripted testing structurally cannot.
- What problem does it solve? The false confidence of a green suite. It solves the gap between “the software passes every check we wrote” and “the software is actually correct” — a gap made entirely of edge cases no one anticipated.
- What are the trade-offs? It is powerful for discovery but not repeatable and hard to put in CI; its quality depends heavily on the tester’s skill and intuition; and without charters, time-boxes, and notes it degrades into untraceable, unaccountable button-mashing.
- When should I avoid it? As a regression gate or a repeatable release check — that is scripted testing’s job. Also avoid leaning on it where a precise, pre-known specification must be verified identically on every build; a script does that better and cheaper.
- What breaks if I remove it? Your only source of new bug discovery. Remove it and you are left checking a fixed set of known expectations forever — every genuinely surprising edge case ships straight to users, because nothing in your process was ever trying to find the unthought-of.
Check your understanding
Section titled “Check your understanding”- State the classic three-part definition of exploratory testing, and explain what “steering” means in the loop.
- Using the known/unknown grid, explain the one cell that scripted testing structurally cannot reach, and why.
- Name the three structural tools of session-based test management and say what each one contributes — direction, focus, or accountability.
- Why is “ad-hoc button-mashing” not the same as exploratory testing? What is the difference?
- Exploratory testing’s great limitation is that it isn’t repeatable. Describe the cycle that resolves this, and state the single habit that makes the cycle work.
Show answers
- Exploratory testing is simultaneous learning, test design, and test execution — the three phases collapsed into one loop instead of running in sequence. Steering means each result redirects the next probe: an odd behavior isn’t noted and abandoned, it’s followed, so the tester’s path through the software is shaped live by what the software actually reveals.
- The unknown unknowns cell — bugs nobody knew to look for. A scripted test is a question written in advance; you cannot write the question for a surprise you haven’t had yet, so scripts can only ever confirm expectations you already hold. Exploration is the only technique aimed directly at surfacing the unanticipated.
- Charters give direction (a one-sentence mission naming the territory, not the steps). Time-boxed sessions give focus (an uninterrupted 60–120 min block on one charter, and a unit you can plan/count). Note-taking gives accountability (a reviewable record of what was tried and found, since there’s no script to point back at — and the raw material for new automated tests).
- Button-mashing has no hypothesis and no memory — it hits bugs by luck. Exploratory testing is directed by the tester’s model of how the system is built and where such systems fail; each probe tests a hypothesis and each result steers the next. The inputs may be wild, but the thinking is deliberate.
- Cycle: exploratory testing discovers an unknown-unknown bug → you write a scripted test that pins that now-known bug → the test joins the automated regression suite, which guards it cheaply forever → that frees the next exploratory session to hunt new surprises instead of re-checking old ground. The habit that makes it work: every bug exploration finds gets fed back into the automated regression suite as a new scripted test.