🧪 可测试性设计
Composer Skills 的 586 个测试用例(README 对外称 450+)能在无 PHP、无 Composer、无网络的机器上全部跑通,得益于贯穿基础层与 SDK 层的 Mock 机制。
测试规模概览
| 包 | func Test 数量 |
|---|---|
pkg/composer | 422 |
pkg/installer | 78 |
pkg/client | 22 |
pkg/composerutils | 20 |
pkg/detector | 17 |
pkg/repository | 16 |
pkg/domain | 11 |
| 合计 | 586 |
🔌 Mock 机制一:pkg/composer 的 SetupMockOutput
定义在 pkg/composer/composer.go 与 pkg/composer/test_utils.go。
核心思路:用一个包级变量 testMode 标记测试态,在测试态下跳过对真实 composer 二进制的校验;同时用一个全局 map[string]MockOutput 按"命令字符串"注入伪输出。
go
// composer.go
var testMode = false
// 启用测试模式并为特定命令设置模拟输出
func SetupMockOutput(command string, output string, err error) {
// ... 写入 mockOutputs 映射 ...
testMode = true
}go
// 测试中这样用
func TestAudit(t *testing.T) {
composer.SetupMockOutput("composer audit --format=json",
`{"found":2,"advisories":[]}`, nil)
defer composer.ClearMockOutputsAdvanced()
comp, _ := composer.New(composer.DefaultOptions())
result, _ := comp.AuditWithJSON()
assert.Equal(t, 2, result.Found)
}并发安全由 sync.RWMutex(test_utils.go 中的 mockMutex)保证。
🧩 Mock 机制二:pkg/composerutils/mock 的 MockCommandExecutor
面向需要替换命令执行器本身的场景,定义在 pkg/composerutils/mock/mock.go。
go
// CommandExecutor 接口 —— 可替换的命令执行器
type CommandExecutor interface {
Execute(name string, args ...string) ([]byte, error)
}
// 默认实现:真实执行系统命令
type DefaultCommandExecutor struct{}
// Mock 实现:返回预设结果
type MockCommandExecutor struct {
CommandResults map[string]struct{ Output []byte; Err error }
}pkg/installer 的 78 个测试正是通过注入 MockCommandExecutor 来验证"检测到缺失 → 触发安装"的分支逻辑,而不真正下载 PHP。
go
exec := mock.NewMockCommandExecutor()
exec.SetCommandResult("composer", []string{"--version"},
[]byte("Composer version 2.7.0"), nil)⚙️ testMode 的作用
composer.go 第 252 行附近:当 testMode == false(非测试态)时,New() 会强制校验本地 composer 二进制是否存在;测试态下跳过该校验,使测试不依赖真实环境。SetupMockOutput / ClearMockOutputsAdvanced 会自动把 testMode 置为 true。
🗂️ 测试组织
- 就近放置:每个
foo.go旁边都有foo_test.go,61 个测试文件分布在 7 个包中。 - 集成测试:
pkg/composer/integration_test.go用多个 Mock 串联完整工作流(检测→安装→执行→审计)。 - 共享辅助:
pkg/composer/test_utils.go的createTestComposerLock等函数为多个测试文件复用,避免重复造 fixture。
📝 如何为自己的代码写测试
- CLI 封装方法(
pkg/composer):调用composer.SetupMockOutput("composer <子命令> <参数>", <输出>, <错误>)注入伪输出,defer清理,然后断言结构化返回值。 - 安装/检测逻辑(
pkg/installer、pkg/detector):用mock.NewMockCommandExecutor()构造执行器,SetCommandResult预设返回,再走业务逻辑。 - HTTP 客户端(
pkg/client):用标准httptest.NewServer起伪 Packagist 服务,把客户端 base URL 指向它。 - 领域模型(
pkg/domain):纯结构体,直接构造实例做断言即可。
运行测试
bash
make test # 全部测试
make test-race # 竞态检测
make test-coverage # 覆盖率报告注意
Mock 输出是全局共享的,测试之间务必 defer ClearMockOutputsAdvanced() 清理,否则会污染其他用例。