stepwise-testing
Stepwise Testing
Exhaustive verification approach that validates every step, line of execution, and component - not just end-to-end outcomes. When debugging, instrument each step; when testing, verify every intermediate state; never assume, always verify.
When to use me
Use stepwise testing when:
- Debugging complex failures and need to trace exactly where things break
- Implementing multi-step workflows where each step must be verified
- Testing critical path scenarios in production code
- Investigating intermittent bugs that require exhaustive tracing
- Building complex features where components interact
- Establishing test coverage for new codebases
What I do
1. Debug Instrumentation
- Add print/log statements at every decision point, function entry/exit, and state change
- Trace data flow through the entire call stack
- Log variable states before/after each transformation
- Capture intermediate results for comparison
2. Stepwise Verification
- Test each function individually with edge cases and boundary conditions
- Verify data transformations at each step, not just final output
- Ensure state transitions only occur under valid conditions
- Validate side effects (file IO, network calls, memory operations)
3. Component Isolation
- Break complex flows into testable units
- Mock dependencies to verify interface contracts
- Test error paths and failure scenarios for each component
- Verify resource lifecycle (cleanup, leaks, handles)
4. Exhaustive Coverage
- Don't skip steps just because they "should" work
- Verify assumptions about third-party libraries/APIs
- Test concurrency and race conditions where applicable
- Validate error handling and exception recovery
Examples
Debugging with Stepwise Tracing
Before (missing details):
def process_user(data):
validated = validate(data)
result = transform(validated)
save(result)
return result
After (stepwise instrumentation):
def process_user(data):
print(f"[ENTER] process_user with data: {data}")
# Step 1: Validation
print("[STEP] Starting validation...")
validated = validate(data)
print(f"[AFTER] validate returned: {validated}, keys: {validated.keys()}")
# Step 2: Transformation
print("[STEP] Starting transformation...")
result = transform(validated)
print(f"[AFTER] transform returned: {result}")
# Step 3: Persistence
print("[STEP] About to save result...")
save(result)
print(f"[SUCCESS] Saved result")
print(f"[EXIT] process_user returning: {result}")
return result
Testing Each Step Individually
Don't just test the final output:
# Bad - only tests final result
def test_process_user():
result = process_user({"name": "test"})
assert result["status"] == "success"
Test each intermediate step:
# Good - tests each transformation
def test_validate_step():
invalid_data = {"name": ""}
result = validate(invalid_data)
assert result is None or get_error(result) == "name_required"
def test_transform_step():
validated = {"name": "test", "email": "test@example.com"}
result = transform(validated)
assert result is not None
assert "status" in result
assert "processed_at" in result # Timestamp added
def test_save_step():
result = {"name": "test", "email": "test@example.com"}
saved_id = save(result)
assert isinstance(saved_id, str)
# Verify actually saved
retrieved = retrieve(saved_id)
assert retrieved == result
Verifying Complex Multi-Step Workflows
# Example: Payment processing flow
# Step 1: Validate payment method
print("[1/6] Validating payment method...")
payment_method = validate_payment_method(card_data)
print(f" Result: {payment_method.get('status', 'invalid')}")
assert payment_method["status"] == "valid", f"Invalid: {payment_method}"
# Step 2: Check balance/funds
print("[2/6] Checking available balance...")
balance = check_balance(payment_method["account_id"])
print(f" Balance: {balance}/ Required: {amount}")
assert balance >= amount, f"Insufficient funds"
# Step 3: Hold funds
print("[3/6] Placing funds on hold...")
hold_id = place_hold(payment_method["account_id"], amount)
print(f" Hold ID: {hold_id}")
assert hold_id is not None
# Step 4: Execute transfer
print("[4/6] Executing transfer...")
transfer = execute_transfer(hold_id, recipient_account)
print(f" Transfer ID: {transfer['id']}")
assert transfer["status"] == "pending"
# Step 5: Verify receipt
print("[5/6] Verifying recipient received funds...")
recipient_check = check_transaction(transfer["id"])
print(f" Recipient status: {recipient_check['status']}")
assert recipient_check["status"] == "completed"
# Step 6: Finalize
print("[6/6] Finalizing and releasing hold...")
finalize(transfer["id"])
print(" ✅ Complete")
Network Request Verification
Don't assume a library works:
# Don't just use requests library
response = requests.get(url)
assert response.ok
# Verify each step:
print("[1/3] Testing DNS resolution...")
import socket
try:
ip = socket.gethostbyname(urlparse(url).hostname)
print(f" Resolved to: {ip}")
except socket.gaierror as e:
print(f" ❌ DNS failed: {e}")
raise
print("[2/3] Testing TCP connection...")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
try:
sock.connect((ip, 443))
print(f" ✅ Connected")
except Exception as e:
print(f" ❌ Connection failed: {e}")
raise
finally:
sock.close()
print("[3/3] Testing HTTP handshake...")
response = requests.get(url, timeout=10)
print(f" Status: {response.status_code}")
print(f" Headers: {dict(response.headers)}")
print(f" Body length: {len(response.text)}")
assert response.ok, f"HTTP error: {response.status_code}"
Output format
Stepwise testing produces detailed, line-by-line output:
[STEP 1/5] Validating input parameters...
Input: {'user_id': 123, 'amount': 50.0}
✅ All required fields present
✅ Data types valid
[STEP 2/5] Checking user permissions...
User role: 'standard'
Permission check: 'can_withdraw' -> False
❌ Permission denied
[FAIL] User 123 cannot withdraw permissions
Debugging Patterns
Pattern 1: Function Entry/Exit
def function_name(arg1, arg2):
print(f"[ENTER] function_name: arg1={arg1}, arg2={arg2}")
try:
result = # ... work ...
print(f"[EXIT] function_name -> {result}")
return result
except Exception as e:
print(f"[ERROR] function_name: {e}")
raise
Pattern 2: Loop Instrumentation
for i, item in enumerate(items):
print(f"[LOOP] Iteration {i}/{len(items)}: item={item}")
result = process(item)
print(f"[LOOP] Result: {result}")
Pattern 3: State Changes
print(f"[STATE] Before: counter={counter}")
counter += 1
print(f"[STATE] After: counter={counter}")
Pattern 4: Conditional Branches
if condition:
print(f"[BRANCH] Taking 'if' path: condition={condition}")
# ... work ...
else:
print(f"[BRANCH] Taking 'else' path: condition={condition}")
# ... work ...
Testing Patterns
Pattern 1: Unit Test Each Function
def test_validate():
# Valid input
assert validate({"name": "test"}) is not None
# Missing field
assert validate({"missing": True}) is None
# Invalid type
assert validate({"name": []}) is None
def test_transform():
# Normal case
assert transform({"name": "test"}) is not None
# Test data transformation
result = transform({"name": "test"})
assert "processed_at" in result
Pattern 2: Verify Assumptions
def test_third_party_behavior():
"""Don't assume the library behaves as documented"""
response = library.action(input)
# Verify the actual behavior
assert response.structure == expected
assert response.timing < threshold
Pattern 3: Edge Cases
def test_edge_cases():
# Empty input
assert transform({}) == expected_empty
# Very large input
assert transform(large_data) == expected_large
# Unicode/special chars
assert transform({"name": "🚀"}) == expected_unicode
Implementation Guidelines
1. Add Instrumentation First
Before adding complex logic, instrument the simple version:
def simple():
print("[START]")
print("[STEP 1] ...")
print("[STEP 2] ...")
print("[DONE]")
2. Test Each Component
Never combine untested components. Test separately first:
# Test A alone
result_a = component_a()
# Test B alone
result_b = component_b(result_a)
# Only then integrate
integrated = integrate(result_b)
3. Verify External Dependencies
Don't trust third-party code:
# Verify it actually works
test_result = external_library.call(test_input)
assert test_result == expected_external_behavior
4. Capture Evidence
Save intermediate states for inspection:
debug_states = []
for step in workflow:
state_before = copy.deepcopy(current_state)
result = execute(step)
state_after = copy.deepcopy(current_state)
debug_states.append({
"step": step,
"before": state_before,
"result": result,
"after": state_after
})
Notes
- Stepwise testing takes time but saves debugging time later
- Instrument everywhere - you can remove it after debugging
- Test assumptions about libraries/frameworks - don't assume documentation is accurate
- Log both success and failure - knowing where it succeeded is as important as where it failed
- Keep it granular - one assertion/verification per line where practical
- Version control the instrumentation - you can comment it out but keep it for future debugging
- Use debug levels - ERROR, WARN, INFO, DEBUG, TRACE for different detail levels
More from wojons/skills
adversarial-thinking
Apply systematic adversarial thinking patterns including devil's advocate, assumption busting, red teaming, and white hat security approaches
45devils-advocate
Challenge ideas, assumptions, and decisions by playing devil's advocate to identify weaknesses and prevent groupthink
41redteam
Think and act like an attacker to identify security vulnerabilities, weaknesses, and penetration vectors through adversarial security testing
37code-migration
Guide framework and library migrations with incremental strategies, breaking change analysis, compatibility testing, and automated migration tools
34observability-logging
Use logs as part of comprehensive observability strategy including metrics, traces, alerts, and dashboards for system understanding and operational excellence
34gap-analysis
Identify discrepancies between documented requirements and actual implementation through systematic comparison and analysis
34