概念定义
遗留代码就是没有测试的代码,未经证实就去改它等于抡锤考古。特征测试把观察到的行为变成可执行事实,让重构有绊线。
工作机制
选定将要改动的接缝;输入代表性用例、捕获真实输出、据此写断言——包括令人意外的那些。被钉住的 bug 断言要显式标记,日后作为“有意的行为变更”单独修复。在网内重构;脚手架测试不再挣得维护成本就删除。
使用场景
对未测代码做任何结构性变更之前:抽取依赖、性能重写、框架迁移。替代方案“徒手重构加肉眼 diff”正是清理期间行为静默漂移的方式。
常见误区
特征测试验证的是稳定不是正确——断言观察到的行为(含缺陷)本来就是要点。把所有特征测试当永久回归测试保留,等于从救援行动继承一批维护欠账。过度钉化会把怪癖冻成契约;钉在实现内部而非可观察边界,会让每次合法重构都触网;快照式批量特征化会同时放大两个问题。
代码示例
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 main
import (
"fmt"
"strings"
"testing"
)
func normalizeEmail(raw string) (string, error) {
email := strings.TrimSpace(strings.ToLower(raw))
at := strings.IndexByte(email, '@')
if at <= 0 || at == len(email)-1 || strings.Count(email, "@") != 1 {
return "", fmt.Errorf("invalid email %q", raw)
}
return email, nil
}
func TestNormalizeEmail(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input string
want string
wantErr bool
}{
{"lowercases and trims", " [email protected] ", "[email protected]", false},
{"rejects missing at", "ada.example.com", "", true},
{"rejects empty local part", "@example.com", "", true},
{"rejects double at", "ada@@example.com", "", true},
}
for _, testCase := range cases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
got, err := normalizeEmail(testCase.input)
if testCase.wantErr {
if err == nil {
t.Fatalf("expected error, got %q", got)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != testCase.want {
t.Fatalf("got %q, want %q", got, testCase.want)
}
})
}
}
func ExampleNormalizeEmail() {
email, _ := normalizeEmail(" [email protected] ")
fmt.Println(email)
// Output: [email protected]
}