← 中文资源

简体中文版本

测试顺序依赖

测试顺序依赖是测试间的隐藏耦合——前者留下状态、后者默默消费——只在执行器打乱或并行时以随机红灯的形式现形。

阅读英文原文

概念定义

套件不再是独立实验的集合,而是一条意外流水线:每个测试假设前驱的残留。执行顺序的任何变化——分片、乱序、按字母序插入新测试——都让假设失效。

工作机制

耦合渠道:共享数据库状态、模块与类级可变状态、先前测试设置的环境与配置、文件系统残留。检测靠多排列运行——随机顺序插件标记配对,bisect 模式隔离最小失败序列;修复落在隔离层——每测试事务、模块重新导入、显式重置夹具。

使用场景

启用并行之前、引入重试策略之后、以及“失败聚在特定顺序而非特定测试”之时。CI 定期随机排序能防止耦合重新生长。

常见误区

“本地通过就正确”是错觉:本地通常固定同一顺序,耦合只是在等第一次重排。第一反应“改回能过的顺序”把隐形图固化为政策,重试也只会掩蔽。耦合不只来自共享数据库——单例、类重载、环境变量、时钟状态同样算数。并行分片让同一对测试只在恰好同时调度时相撞,产出最难懂的 flaky 签名。

代码示例

import tempfile
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator, Optional


class SandboxEscapeError(ValueError):
    """Raised when a requested path would leave the sandbox root."""


@dataclass(frozen=True)
class Sandbox:
    root: Path

    def resolve(self, *parts: str) -> Path:
        target = self.root.joinpath(*parts).resolve()
        if target != self.root and self.root not in target.parents:
            raise SandboxEscapeError("path escapes the sandbox: " + str(target))
        return target

    def write(self, relative_path: str, content: str) -> Path:
        target = self.resolve(relative_path)
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(content, encoding="utf-8")
        return target

    def read(self, relative_path: str) -> str:
        return self.resolve(relative_path).read_text(encoding="utf-8")

    def exists(self, relative_path: str) -> bool:
        return self.resolve(relative_path).exists()


@contextmanager
def sandbox(prefix: Optional[str] = None) -> Iterator[Sandbox]:
    """Yield a private temporary sandbox and always remove it afterwards."""
    with tempfile.TemporaryDirectory(prefix=prefix or "devfactory-") as raw:
        yield Sandbox(root=Path(raw).resolve())
package calc

import "testing"

func Divide(a, b float64) (float64, bool) {
	if b == 0 {
		return 0, false
	}
	return a / b, true
}

func TestDivide(t *testing.T) {
	cases := []struct {
		name    string
		a, b    float64
		want    float64
		wantOK  bool
	}{
		{name: "even", a: 10, b: 2, want: 5, wantOK: true},
		{name: "fraction", a: 1, b: 4, want: 0.25, wantOK: true},
		{name: "divide by zero", a: 1, b: 0, want: 0, wantOK: false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, ok := Divide(tc.a, tc.b)
			if ok != tc.wantOK || got != tc.want {
				t.Fatalf("got (%v, %v), want (%v, %v)", got, ok, tc.want, tc.wantOK)
			}
		})
	}
}