| name | bach-exploratory-testing |
| description | Test software in the style of James Bach, pioneer of exploratory testing and context-driven testing. Emphasizes skilled human investigation, heuristics-based test design, and adapting to context rather than following rigid scripts. Use when designing test strategies, performing exploratory testing, or building thinking testers. |
| tags | exploratory-testing, test-design, heuristics, risk-based, session-based, manual-testing, quality, critical-thinking |
James Bach Exploratory Testing Style Guide
Overview
James Bach is a pioneer of exploratory testing and co-founder of the context-driven testing movement. A self-taught software tester who became one of the most influential voices in the field, he advocates for testing as a skilled intellectual activity rather than a mechanical process. His Rapid Software Testing methodology, developed with Michael Bolton, emphasizes simultaneous learning, test design, and execution.
Core Philosophy
"Testing is not a phase. Testing is not a checklist. Testing is the infinite process of comparing a product to what it ought to be."
"Exploratory testing is simultaneous learning, test design, and test execution."
"A good tester is not a person who follows scripts. A good tester is a person who can think."
Bach rejects the notion that testing can be reduced to following predetermined steps. Real testing requires sapient (thinking) humans who adapt their approach based on what they discover. The tester's mind is the primary testing tool.
Design Principles
-
Context Drives Practice: There is no universal best practice—only practices that fit the context.
-
Heuristics, Not Rules: Use fallible methods that usually work, but remain aware they can fail.
-
Skill Over Process: Invest in tester skill development, not just test process documentation.
-
Oracles Are Human Judgments: We recognize problems through principles and heuristics, not just requirements.
-
Testing Is Investigation: Approach testing as a detective, not a factory worker.
The Seven Principles of Context-Driven Testing
1. The value of any practice depends on its context.
2. There are good practices in context, but no best practices.
3. People, working together, are the most important part of any project's context.
4. Projects unfold over time in ways that are often not predictable.
5. The product is a solution. If the problem isn't solved, the product doesn't work.
6. Good software testing is a challenging intellectual process.
7. Only through judgment and skill can we do the right things at the right times.
Heuristic Test Strategy Model (HTSM)
Bach's framework for thinking about test strategy:
┌─────────────────────────────────────────────────────────────┐
│ PROJECT ENVIRONMENT │
│ Customers, Information, Developer Relations, Test Team, │
│ Equipment & Tools, Schedule, Test Items, Deliverables │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PRODUCT ELEMENTS │
│ Structure, Function, Data, Platform, Operations │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ QUALITY CRITERIA │
│ Capability, Reliability, Usability, Charisma, │
│ Security, Scalability, Compatibility, Performance, │
│ Installability, Maintainability │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ TEST TECHNIQUES │
│ Function, Domain, Stress, Flow, Scenario, Claims, │
│ User, Risk, Automatic │
└─────────────────────────────────────────────────────────────┘
When Testing
Always
- Begin with a testing charter or mission
- Document your mental model of the product
- Use oracles to identify potential problems
- Take notes during sessions (not after)
- Time-box exploratory sessions (60-90 minutes)
- Debrief after sessions to capture learnings
- Question requirements—they are often incomplete
Never
- Follow scripts blindly without thinking
- Assume requirements are complete or correct
- Confuse test execution with testing
- Believe that passing tests means quality
- Stop exploring when you find one bug
- Treat automation as a replacement for thinking
- Assume absence of evidence is evidence of absence
Prefer
- Questions over assumptions
- Exploration over confirmation
- Learning over procedure
- Skill over certification
- Models over checklists
- Collaboration over documentation
- Oracles over expected results
Code Patterns
Session-Based Test Management
class ExploratorySession:
"""
A time-boxed exploratory testing session.
Bach's SBTM: structured freedom for skilled testers.
"""
def __init__(self,
charter: str,
duration_minutes: int = 90,
tester: str = None):
self.charter = charter
self.duration = duration_minutes
self.tester = tester
self.start_time = None
self.end_time = None
self.notes = []
self.bugs = []
self.questions = []
self.risks = []
self.areas_explored = []
self.session_metrics = {}
def start(self):
"""Begin the session."""
self.start_time = datetime.now()
self.log(f"Session started. Charter: {self.charter}")
def log(self, observation: str, category: str = 'note'):
"""
Log observations during the session.
Categories: note, bug, question, risk, idea
"""
entry = {
'timestamp': datetime.now(),
'elapsed': .elapsed_minutes(),
: category,
: observation
}
.notes.append(entry)
category == :
.bugs.append(entry)
category == :
.questions.append(entry)
category == :
.risks.append(entry)
() -> :
.start_time:
(datetime.now() - .start_time).total_seconds() /
() -> :
(, .duration - .elapsed_minutes())
():
.end_time = datetime.now()
.session_metrics = {
: .duration,
: .elapsed_minutes(),
: (.bugs),
: (.questions),
: (.risks),
: (.areas_explored),
: (.notes)
}
.log()
.generate_report()
() -> SessionReport:
SessionReport(
charter=.charter,
tester=.tester,
duration=.session_metrics[],
bugs=.bugs,
questions=.questions,
risks=.risks,
areas=.areas_explored,
notes=.notes,
metrics=.session_metrics
)
:
():
.target = target
.resources = resources
.information = information_sought
.time_box = time_box
.priority = priority
() -> :
(
)
() -> :
cls(
target=product_area,
resources=[, , risk.related_technique],
information_sought=,
priority=risk.severity
)
Oracle Heuristics
class OracleHeuristics:
"""
Oracles: principles or mechanisms by which we recognize problems.
Bach's FEW HICCUPPS mnemonic for consistency oracles.
"""
CONSISTENCY_ORACLES = {
'Familiarity': 'Consistent with what testers have seen before',
'Explainability': 'Consistent with what can be explained/documented',
'World': 'Consistent with how the real world works',
'History': 'Consistent with past versions of the product',
'Image': 'Consistent with the organization\'s desired image',
'Comparable_Products': 'Consistent with similar products',
'Claims': 'Consistent with documentation, ads, specs',
'User_Expectations': 'Consistent with what users want/expect',
'Product': 'Consistent with itself (internal consistency)',
'Purpose': 'Consistent with the explicit/implicit purpose',
'Statutes': 'Consistent with laws, regulations, standards',
}
def __init__(self):
self.oracle_applications = []
def apply_oracle(self,
observation: str,
oracle_type: str,
expected_consistency: str,
actual_behavior: str) -> OracleResult:
"""
Apply an oracle to evaluate observed behavior.
"""
is_consistent = .evaluate_consistency(
expected_consistency,
actual_behavior
)
result = OracleResult(
oracle=oracle_type,
observation=observation,
expected=expected_consistency,
actual=actual_behavior,
consistent=is_consistent,
confidence=.assess_confidence(oracle_type),
notes=[]
)
.oracle_applications.append(result)
result
() -> :
() -> :
high_confidence = [, , ]
medium_confidence = [, , ]
low_confidence = [, , ]
oracle_type high_confidence:
oracle_type medium_confidence:
:
() -> []:
ideas = []
oracle, description .CONSISTENCY_ORACLES.items():
ideas.append(
)
ideas
:
ELEMENTS = {
: [
,
,
,
,
,
],
: [
,
,
,
,
,
],
: [
,
,
,
,
,
],
: [
,
,
,
,
,
],
: [
,
,
,
,
,
],
: [
,
,
,
,
,
]
}
():
.product = product_name
.coverage_map = {element: [] element .ELEMENTS}
.explored_areas = ()
():
element .coverage_map:
.coverage_map[element].extend(specifics)
():
.explored_areas.add((element, specific, session_id))
() -> [, []]:
gaps = {}
explored_specifics = {(e, s) e, s, _ .explored_areas}
element, specifics .coverage_map.items():
unexplored = [s s specifics (element, s) explored_specifics]
unexplored:
gaps[element] = unexplored
gaps
() -> []:
charters = []
gaps = .coverage_gaps()
element, specifics gaps.items():
specific specifics[:]:
charters.append(
)
charters
Test Heuristics
class TestHeuristics:
"""
Heuristics are fallible methods for solving problems.
Bach's testing heuristics for generating test ideas.
"""
ZOMBIE = {
'Zero': 'Test with zero, empty, null, none',
'One': 'Test with one, single, first, minimum',
'Many': 'Test with many, multiple, maximum, large',
'Boundary': 'Test at boundaries, edges, limits',
'Interface': 'Test at interfaces, handoffs, integrations',
'Exceptions': 'Test error conditions, invalid inputs, edge cases'
}
QUALITY_CRITERIA = {
'Capability': 'Can it perform its functions?',
'Reliability': 'Will it work consistently?',
'Usability': 'Can real users use it?',
'Security': 'Is it protected from threats?',
'Scalability': 'Does it handle growth?',
'Performance': 'Is it fast enough?',
'Installability': 'Can it be deployed?',
'Compatibility': 'Does it work with other things?',
'Supportability': 'Can it be maintained?',
'Testability': 'Can it be tested effectively?',
'Maintainability': 'Can it be changed?',
'Portability': 'Does it work in different environments?',
:
}
() -> []:
tests = []
data_type == :
tests.extend([
,
,
,
,
,
,
,
,
,
])
data_type == :
tests.extend([
,
,
,
,
,
,
,
,
,
])
data_type == :
tests.extend([
,
,
,
,
,
,
,
])
tests
() -> [, []]:
tests = {}
criterion, question .QUALITY_CRITERIA.items():
tests[criterion] = [
,
,
,
]
tests
() -> []:
[
,
,
,
,
,
,
,
,
]
() -> [, ]:
{
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
}
Rapid Software Testing Session
class RapidTestingSession:
"""
Rapid Software Testing: Bach & Bolton's methodology.
Testing as a performance, not a procedure.
"""
def __init__(self,
product: str,
tester: 'SkilledTester',
stakeholder_info: Dict):
self.product = product
self.tester = tester
self.stakeholder = stakeholder_info
self.mental_model = ProductCoverageModel(product)
self.oracles = OracleHeuristics()
self.heuristics = TestHeuristics()
self.sessions = []
self.findings = []
def analyze_context(self) -> ContextAnalysis:
"""
Understand the project context before testing.
"""
return ContextAnalysis(
who_is_the_customer=self.stakeholder.get('customer'),
what_is_the_mission=self.stakeholder.get('mission'),
what_does_quality_mean=self.stakeholder.get('quality_definition'),
what_threatens_quality=self.stakeholder.get('risks'),
what_resources_exist=self.stakeholder.get('resources'),
what_constraints_exist=self.stakeholder.get('constraints'),
what_has_been_done=self.stakeholder.get('prior_testing'),
)
() -> TestStrategy:
strategy = TestStrategy(product=.product)
risk context.what_threatens_quality:
strategy.add_focus_area(
area=risk.area,
priority=risk.severity,
techniques=.select_techniques(risk),
time_allocation=.estimate_time(risk)
)
area strategy.focus_areas:
charters = .generate_charters(area)
strategy.add_charters(area.name, charters)
strategy
() -> []:
techniques = []
risk.involves_boundaries:
techniques.append()
risk.involves_user_interaction:
techniques.append()
techniques.append()
risk.involves_data:
techniques.append()
techniques.append()
risk.involves_integration:
techniques.append()
techniques.append()
risk.involves_time:
techniques.append()
techniques.append()
techniques.append()
techniques
() -> SessionReport:
session = ExploratorySession(
charter=(charter),
duration_minutes=charter.time_box,
tester=.tester.name
)
session.start()
.sessions.append(session)
session
() -> DebriefNotes:
report = session.generate_report()
debrief = DebriefNotes(
session_id=(session),
what_was_tested=session.areas_explored,
what_was_found=session.bugs,
what_was_learned=.extract_learnings(session),
what_should_change=.recommend_changes(session),
new_questions=session.questions,
new_risks=session.risks,
)
area session.areas_explored:
.mental_model.mark_explored(
area[],
area[],
((session))
)
debrief
() -> []:
learnings = []
note session.notes:
note[].lower():
learnings.append(note[])
note[].lower():
learnings.append(note[])
note[].lower():
learnings.append(note[])
learnings
() -> []:
recommendations = []
session.session_metrics[] > :
recommendations.append(
)
session.session_metrics[] > :
recommendations.append(
)
recommendations
Mental Model
Bach approaches testing by asking:
- What is the mission? Who cares, and what do they need to know?
- What could go wrong? Risks drive test focus
- How will I recognize a problem? Which oracles apply?
- What have I covered? Maintain a mental model of the product
- What did I learn? Each test teaches something
The Session Checklist
□ Charter defined (Explore X with Y to discover Z)
□ Time-box set (60-90 minutes typical)
□ Oracles identified (how will I recognize problems?)
□ Note-taking ready (real-time, not after)
□ Product model in mind (SFDPOT coverage)
□ Heuristics available (ZOMBIE, touring, soap opera)
□ Questions captured (for stakeholder follow-up)
□ Debrief scheduled (capture learnings immediately)
Signature Bach Moves
- Session-Based Test Management (SBTM)
- Heuristic Test Strategy Model (HTSM)
- FEW HICCUPPS consistency oracles
- SFDPOT product coverage model
- Exploratory testing charters
- Context-driven approach (no best practices)
- Soap Opera Testing for complex scenarios
- Touring heuristics for systematic exploration