Skip to content

Exploratory Testing Deep Dive — Beyond Scripted Testing

DodaTech Updated 2026-06-24 10 min read

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

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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