边学习边测试
探索式测试把测试设计视为执行过程中发生的活动。测试人员形成关于产品行为的假设,尝试它,观察结果,并立即调整下一个实验。这与预先固定步骤的脚本化测试相反。其价值恰好出现在没人写过脚本的地方:异常操作序列、被中断的流程、权限边界和令人困惑的状态。
结构避免随机
好的探索是有纪律的。章程给出任务,例如“探查部分扣款后的退款”;时间盒通常为六十到九十分钟;笔记记录尝试了什么、观察到了什么、还有哪些未探索。基于会话的测试管理把会话汇总起来,使覆盖可见,并能通过复盘质疑假设。没有结构,探索只会变成轶事而非证据。
技能与证据
测试人员需要产品知识、技术好奇心和识别预言的能力:在这里什么算错误。证据很重要,要捕获请求、截图、日志和触发缺陷的精确序列。可复现的路径才能带来修复。把探索会话与可观测性工具结合,能让测试人员看到界面隐藏的服务端影响。
何时使用与局限
新功能、模糊需求、第三方集成以及用户可能行为异常的区域都适合探索。它补充自动化:与脚本化回归测试相比,探索能发现未知的未知,但覆盖不易重复,因此有价值的发现应转化为自动化检查。探索无法低成本覆盖数千条回归,结果高度依赖测试人员水平,也不能替代负载、安全或契约测试。例如,一次会话可能发现令人困惑的错误提示,却不会发现持续流量下的内存泄漏。
代码示例
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Protocol
class Clock(Protocol):
def now(self) -> datetime: ...
class SystemClock:
def now(self) -> datetime:
return datetime.now(timezone.utc)
class FakeClock:
def __init__(self, start: datetime) -> None:
if start.tzinfo is None:
raise ValueError("start must be timezone aware")
self._current = start
def now(self) -> datetime:
return self._current
def advance(self, seconds: float) -> None:
self._current += timedelta(seconds=seconds)
@dataclass
class CachedValue:
clock: Clock
ttl: timedelta
value: str | None = None
expires_at: datetime | None = None
def get(self) -> str | None:
now = self.clock.now()
if self.value is None or self.expires_at is None or now >= self.expires_at:
return None
return self.value
def put(self, value: str) -> None:
self.value = value
self.expires_at = self.clock.now() + self.ttl
def main() -> None:
clock = FakeClock(datetime(2026, 10, 21, 12, 0, tzinfo=timezone.utc))
cache = CachedValue(clock=clock, ttl=timedelta(seconds=30))
cache.put("fresh")
print("immediately:", cache.get())
clock.advance(29)
print("after 29s:", cache.get())
clock.advance(2)
print("after 31s:", cache.get())
print("system clock tz:", SystemClock().now().tzinfo)
if __name__ == "__main__":
main()
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone)]
struct FakeClock {
millis: Arc<AtomicU64>,
}
impl FakeClock {
fn new() -> Self {
Self {
millis: Arc::new(AtomicU64::new(0)),
}
}
fn now(&self) -> Duration {
Duration::from_millis(self.millis.load(Ordering::SeqCst))
}
fn advance(&self, delta: Duration) {
self.millis
.fetch_add(delta.as_millis() as u64, Ordering::SeqCst);
}
}
struct Token {
expires_at: Duration,
}
impl Token {
fn issue(clock: &FakeClock, ttl: Duration) -> Self {
Self {
expires_at: clock.now() + ttl,
}
}
fn expired(&self, clock: &FakeClock) -> bool {
clock.now() >= self.expires_at
}
}
fn main() {
let clock = FakeClock::new();
let token = Token::issue(&clock, Duration::from_secs(60));
clock.advance(Duration::from_secs(59));
println!("expired at 59s: {}", token.expired(&clock));
clock.advance(Duration::from_secs(1));
println!("expired at 60s: {}", token.expired(&clock));
}