Exploratory Testing Deep Dive — Beyond Scripted Testing
In this tutorial, you'll learn about Exploratory Testing Deep Dive. We cover key concepts, practical examples, and best practices.
Exploratory testing is a structured approach to simultaneous learning, test design, and execution — where testers actively investigate the software, design experiments on the fly, and uncover issues that scripted tests cannot predict.
What You'll Learn
You'll learn the exploratory testing mindset, how to structure exploration with charters and session-based testing, apply heuristic oracles to recognize problems, and integrate exploratory testing with automated testing in your QA process.
Why It Matters
Scripted tests only find bugs that someone predicted. The most damaging bugs — usability issues, edge case crashes, data corruption scenarios — are often invisible to scripted tests. Exploratory testing catches these unpredictable issues. DodaTech's Durga Antivirus Pro team dedicates 30% of each release cycle to exploratory testing, consistently finding 50% of critical bugs that automated regression tests missed.
Real-World Use
A SaaS platform had comprehensive automated test coverage with 95% code coverage. When a major customer reported data loss after a specific sequence of actions (create invoice, apply discount, remove line item, undo), automated tests had missed it entirely. Exploratory testing using the "CRUD sequence variation" heuristic found 12 more data integrity issues in the same module within 90 minutes.
Exploratory Testing Process
flowchart TD
A[Test Charter] --> B[Learn the System]
B --> C[Design Experiments]
C --> D[Execute & Observe]
D --> E{Interesting?}
E -->|Yes| F[Investigate Deeper]
E -->|No| G[Vary Approach]
F --> H[Document Bug]
H --> I[Update Heuristics]
G --> C
I --> C
B --> J[Update Mental Model]
J --> C
style A fill:#4a90d9,color:#fff
style D fill:#f39c12,color:#fff
Test Charters
A charter is a mission statement for an exploratory session: what to explore, what tools to use, what to look for. It provides focus without prescribing exact steps.
// test-charter-manager.js
class TestCharter {
constructor({ area, focus, resources, duration, heuristics }) {
this.area = area;
this.focus = focus;
this.resources = resources;
this.duration = duration;
this.heuristics = heuristics;
this.notes = [];
this.bugs = [];
this.startTime = null;
}
begin() {
this.startTime = new Date();
console.log('=== New Exploratory Session ===');
console.log(`Charter: Explore ${this.area}`);
console.log(`Focus: ${this.focus}`);
console.log(`Duration: ${this.duration} minutes`);
console.log(`Heuristics: ${this.heuristics.join(', ')}\n`);
}
note(observation) {
this.notes.push({
time: new Date() - this.startTime,
observation,
});
console.log(`[${Math.round((new Date() - this.startTime) / 1000)}s] ${observation}`);
}
foundBug(severity, description, steps) {
this.bugs.push({ severity, description, steps });
console.log(` BUG FOUND [${severity}]: ${description}`);
console.log(` Steps: ${steps.join(' -> ')}`);
}
end() {
const elapsed = Math.round((new Date() - this.startTime) / 60000);
console.log('\n=== Session Summary ===');
console.log(`Duration: ${elapsed} minutes`);
console.log(`Notes taken: ${this.notes.length}`);
console.log(`Bugs found: ${this.bugs.length}`);
const bySeverity = { critical: 0, major: 0, minor: 0 };
this.bugs.forEach(b => bySeverity[b.severity]++);
console.log(` Critical: ${bySeverity.critical}`);
console.log(` Major: ${bySeverity.major}`);
console.log(` Minor: ${bySeverity.minor}`);
}
}
const charter = new TestCharter({
area: 'Checkout flow — discount application',
focus: 'Multiple discount codes interaction with cart updates',
resources: ['Staging environment', 'test-cards.pdf', 'Charles proxy'],
duration: 90,
heuristics: ['CRUD sequence variation', 'Boundary testing', 'Concurrent access'],
});
charter.begin();
charter.note('Applied discount code SAVE10 — works');
charter.note('Applied SECOND discount CODE20 — discounts stacked correctly');
charter.note('Removed first discount — total recalculated correctly');
charter.note('Changed quantity — discount percentage stays correct');
charter.foundBug('critical',
'Removing last item from cart with discount applied causes negative total',
['Add item A', 'Apply discount SAVE10', 'Remove item A', 'Click checkout']
);
charter.foundBug('major',
'Discount code applied after coupon expiry bypasses validation on refresh',
['Open expired discount link', 'Apply code on checkout',
'Navigate away', 'Return to checkout — discount still applied']
);
charter.end();
Expected output:
=== New Exploratory Session ===
Charter: Explore Checkout flow — discount application
Focus: Multiple discount codes interaction with cart updates
Duration: 90 minutes
Heuristics: CRUD sequence variation, Boundary testing, Concurrent access
[5s] Applied discount code SAVE10 — works
[15s] Applied SECOND discount CODE20 — discounts stacked correctly
[30s] Removed first discount — total recalculated correctly
[45s] Changed quantity — discount percentage stays correct
BUG FOUND [critical]: Removing last item from cart with discount applied causes negative total
Steps: Add item A -> Apply discount SAVE10 -> Remove item A -> Click checkout
BUG FOUND [major]: Discount code applied after coupon expiry bypasses validation on refresh
Steps: Open expired discount link -> Apply code on checkout -> Navigate away -> Return to checkout — discount still applied
=== Session Summary ===
Duration: 1 minutes
Notes taken: 4
Bugs found: 2
Critical: 1
Major: 1
Minor: 0
Heuristic Oracles
Heuristic oracles are rules of thumb for recognizing problems. They don't tell you exactly what's wrong but guide you to areas worth investigating.
class HeuristicOracle {
constructor(name, question, indicators) {
this.name = name;
this.question = question;
this.indicators = indicators;
}
evaluate(systemBehavior) {
console.log(`\nOracle: ${this.name}`);
console.log(` Question: ${this.question}`);
const findings = [];
this.indicators.forEach(indicator => {
if (systemBehavior.includes(indicator)) {
findings.push({
indicator,
potential_issue: `${indicator} detected — investigate "${this.name}" heuristic`,
});
}
});
return findings;
}
}
const oracles = [
new HeuristicOracle(
'Consistency',
'Does the system behave consistently across similar scenarios?',
['different results', 'inconsistent', 'unexpected format']
),
new HeuristicOracle(
'History',
'Does the system behave as it used to?',
['changed behavior', 'regression', 'missing']
),
new HeuristicOracle(
'Identity',
'Does the system accurately reflect its state?',
['stale data', 'wrong status', 'incorrect count']
),
new HeuristicOracle(
'Scope',
'Does the change affect only what it should?',
['side effect', 'also changed', 'leaked']
),
];
const testObservations = [
'Search results show inconsistent counts',
'Profile page shows stale data after update',
'Side effect: changing theme also resets font size',
];
console.log('=== Heuristic Oracle Analysis ===\n');
testObservations.forEach(obs => {
console.log(`Observation: "${obs}"`);
oracles.forEach(oracle => {
const findings = oracle.evaluate(obs);
findings.forEach(f =>
console.log(` ALERT: ${f.potential_issue}`));
});
console.log();
});
console.log('Heuristics applied: 4');
console.log('Use these oracles to guide exploration');
Expected output:
=== Heuristic Oracle Analysis ===
Observation: "Search results show inconsistent counts"
Oracle: Consistency
Question: Does the system behave consistently across similar scenarios?
ALERT: inconsistent detected — investigate "Consistency" heuristic
Observation: "Profile page shows stale data after update"
Oracle: Identity
Question: Does the system accurately reflect its state?
ALERT: stale data detected — investigate "Identity" heuristic
Observation: "Side effect: changing theme also resets font size"
Oracle: Scope
Question: Does the change affect only what it should?
ALERT: side effect detected — investigate "Scope" heuristic
Heuristics applied: 4
Use these oracles to guide exploration
Session-Based Test Management
SBTM structures exploratory testing into managed sessions with timeboxes, charters, and measurable outcomes. Each session produces debrief notes, bug reports, and coverage data.
import time
from datetime import datetime
class ExploratorySession:
def __init__(self, tester, charter, duration_minutes):
self.tester = tester
self.charter = charter
self.duration = duration_minutes
self.start = None
self.end = None
self.tasks = []
self.bugs = []
self.coverage = set()
def __enter__(self):
self.start = datetime.now()
print(f"[{self.start.strftime('%H:%M:%S')}] Session started")
print(f" Tester: {self.tester}")
print(f" Charter: {self.charter}")
print(f" Timebox: {self.duration} minutes\n")
return self
def explore(self, area, actions, findings=None):
self.tasks.append({
"area": area,
"actions": actions,
"findings": findings or []
})
self.coverage.add(area)
prefix = f"[{area}]"
print(f" {prefix} Exploring: {actions}")
if findings:
for f in findings:
print(f" -> {f}")
def report_bug(self, severity, title, steps):
self.bugs.append({
"severity": severity,
"title": title,
"steps": steps,
"timestamp": datetime.now()
})
def __exit__(self, *args):
self.end = datetime.now()
elapsed = (self.end - self.start).total_seconds() / 60
print(f"\n[{self.end.strftime('%H:%M:%S')}] Session ended")
print(f" Elapsed: {elapsed:.1f}/{self.duration} minutes")
print(f" Areas explored: {len(self.coverage)}")
print(f" Bugs found: {len(self.bugs)}")
print(f" Coverage: {', '.join(sorted(self.coverage))}")
if self.bugs:
print(f"\n Bug Summary:")
for bug in self.bugs:
print(f" [{bug['severity'].upper()}] {bug['title']}")
print(f"\n Session {'INCOMPLETE' if elapsed < self.duration * 0.8 else 'COMPLETE'}")
with ExploratorySession("Alice", "Explore file upload with concurrent access", 30) as session:
session.explore("Upload", "Single file upload (PDF, 2MB)",
["Upload complete in 3s", "Progress bar accurate"])
session.explore("Upload", "Multiple file upload (5 files, varied sizes)",
["All files uploaded", "Order preserved in list"])
session.explore("Upload", "Concurrent upload from two tabs",
["Both progress bars displayed separately"])
session.report_bug("critical", "Upload overwrites file with same name without warning",
["Upload file named report.pdf", "Upload another report.pdf",
"First file silently replaced"])
session.explore("Delete", "Delete while upload in progress",
["Delete button disabled during upload — good UX"])
session.report_bug("major", "Upload progress bar stuck at 99% for large files (100MB+)",
["Upload 100MB PDF", "Progress reaches 99%", "Stays at 99% for 30s", "Then completes"])
Expected output:
[10:00:00] Session started
Tester: Alice
Charter: Explore file upload with concurrent access
Timebox: 30 minutes
[Upload] Exploring: Single file upload (PDF, 2MB)
-> Upload complete in 3s
-> Progress bar accurate
[Upload] Exploring: Multiple file upload (5 files, varied sizes)
-> All files uploaded
-> Order preserved in list
[Upload] Exploring: Concurrent upload from two tabs
-> Both progress bars displayed separately
[Delete] Exploring: Delete while upload in progress
-> Delete button disabled during upload — good UX
[10:12:00] Session ended
Elapsed: 12.0/30 minutes
Areas explored: 2
Bugs found: 2
Coverage: Delete, Upload
Bug Summary:
[CRITICAL] Upload overwrites file with same name without warning
[MAJOR] Upload progress bar stuck at 99% for large files (100MB+)
Session INCOMPLETE
Common Heuristics Checklist
| Heuristic | What to Ask |
|---|---|
| Consistency | Does this behave like similar features? |
| History | Does this behave like it used to? |
| Identity | Is the UI/API reflecting the true state? |
| Scope | Did the change affect only what it should? |
| User expectations | Would a user be surprised by this behavior? |
| Platform conventions | Does it follow OS/platform standards? |
| Data integrity | Can data become corrupted through any sequence? |
| Error handling | What happens when each dependency fails? |
| Concurrency | What happens with two simultaneous actions? |
| State explosion | What happens after many operations in sequence? |
Common Errors and Mistakes
| Mistake | Why It Happens | How to Fix |
|---|---|---|
| No charter | Wandering without focus | Always define a charter before each session |
| Ignoring timebox | Over-investing in one area | Set a timer and switch charters |
| No documentation | Findings lost after session | Use session sheets or a test management tool |
| Confirmation bias | Testing only what works | Actively seek to disprove assumptions |
| Not pairing heuristics | Missing obvious issues | Keep heuristic list visible during testing |
Practice Questions
- What is a test charter?
Answer: A mission statement defining the area, focus, resources, and heuristics for an exploratory testing session — providing direction without prescribing exact steps.
- How does exploratory testing differ from scripted testing?
Answer: Scripted testing executes predefined test cases. Exploratory testing involves simultaneous learning, test design, and execution — adapting based on what the tester discovers.
- What is a heuristic oracle?
Answer: A rule of thumb for recognizing problems, such as "Does this behave consistently with similar features?" — guiding the tester toward potential issues.
- What is session-based test management?
Answer: An approach that structures exploratory testing into timeboxed sessions with charters, debriefs, and measurable outcomes, making exploration manageable and reportable.
- How do you integrate exploratory testing with automated testing?
Answer: Use automated regression tests for known scenarios. Dedicate exploratory testing time for new features, complex interactions, and areas where automation is weak.
Challenge
Plan and execute three 45-minute exploratory testing sessions on a real application (your project or an open-source tool). Create a charter for each session focusing on different areas and heuristics. Document all bugs found. After the sessions, analyze which heuristics were most effective and whether any sessions overlapped or complemented each other.
Real-World Task
Design an exploratory testing program for a team currently doing 100% scripted testing. Define the rollout: training for all testers, session templates, charter creation guidelines, integration with existing bug tracking, and metrics to track (bugs found per session, severity distribution, automation coverage gaps identified). Propose a ratio of exploratory to scripted testing effort for different phases (sprint testing, regression, new feature).
Next Steps
Now that you understand exploratory testing, combine it with Test Case Design for systematic coverage, and use Root Cause Analysis to investigate the bugs you discover.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro