目的
回归测试套件只回答一个问题:改动之后,原本可用的功能是否被破坏?它不是所有期望行为的规格说明,也不能替代针对新改动本身的测试。它的价值来自跨版本的重复执行。套件要么积累信心,要么积累噪音。
应包含什么
包含稳定、重要且容易破坏的行为:业务规则、API 形状、授权边界、定价计算和关键用户旅程。一次性调查不应进入。优先使用能捕获缺陷的最低层:解析规则用单元测试,事务用集成测试,只有在浏览器本身构成风险时才用浏览器测试。与庞大的端到端套件相比,分层套件能更快定位失败。
选择与速度
完整套件会变慢,因此许多团队维护分层:每次提交运行快速子集,合并前运行更广的范围,夜间运行全量。选择必须保持风险意识;仅仅因为慢就删除测试,等于悄悄移除保护。应测量耗时和失败历史,然后优化或替换昂贵测试,而不是放弃它们。
维护与信任
删除已移除功能的测试;行为有意改变时更新期望并记录原因;及时修复不稳定测试,因为随机失败的套件会训练人们忽略红色构建。把通过率和恢复绿色所需时间作为健康指标,而不仅是覆盖率。例如,如果团队反复重跑失败任务直到通过,套件仍报告成功,却掩盖了真实的竞态。回归套件的价值取决于团队对失败的反应:在排除误报之前,每次红色运行都应被视为信号。
代码示例
import sqlite3
from typing import Iterator
import pytest
@pytest.fixture
def db() -> Iterator[sqlite3.Connection]:
connection = sqlite3.connect(":memory:")
connection.execute(
"CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"
)
try:
yield connection
finally:
connection.close()
def test_insert_item(db: sqlite3.Connection) -> None:
db.execute("INSERT INTO items (name) VALUES (?)", ("widget",))
count = db.execute("SELECT COUNT(*) FROM items").fetchone()[0]
assert count == 1
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));
}