概念定义
夹具是测试运行前必须存在、运行后必须移除的一切:内存对象、数据库记录、临时文件、环境变量、已配置的客户端。建立阶段构造基线,拆除阶段恢复世界。当每个测试拥有或被交付一份全新夹具时,测试之间互不可见,可以按任意顺序甚至并行运行而不相互干扰。
为什么重要
顺序依赖是最常见的不稳定与困惑来源之一:一个单独运行通过的测试,在套件里却失败,因为更早的测试留下了状态。夹具从根本上解决这一点,它让起始条件显式且隔离。它也记录了某段代码对其所处世界的假设,把隐式搭建变成可读的意图。
工程实践
按测试或按作用域创建夹具,并重置共享资源,使状态不在测试间渗出。优先使用快速的内存或容器化夹具,而非生产数据,这既为隐私也为确定性。用小而专的工厂辅助函数构建夹具,让相关字段显式、其余取默认,使每个测试只声明它关心的部分。度量并尽量压缩搭建成本,因为夹具耗时常常主导整个套件。
代码示例
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())
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