Skip to content

🧪 可测试性设计

Composer Skills 的 586 个测试用例(README 对外称 450+)能在无 PHP、无 Composer、无网络的机器上全部跑通,得益于贯穿基础层与 SDK 层的 Mock 机制。

测试规模概览

func Test 数量
pkg/composer422
pkg/installer78
pkg/client22
pkg/composerutils20
pkg/detector17
pkg/repository16
pkg/domain11
合计586

🔌 Mock 机制一:pkg/composerSetupMockOutput

定义在 pkg/composer/composer.gopkg/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.RWMutextest_utils.go 中的 mockMutex)保证。

🧩 Mock 机制二:pkg/composerutils/mockMockCommandExecutor

面向需要替换命令执行器本身的场景,定义在 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.gocreateTestComposerLock 等函数为多个测试文件复用,避免重复造 fixture。

📝 如何为自己的代码写测试

  1. CLI 封装方法pkg/composer):调用 composer.SetupMockOutput("composer <子命令> <参数>", <输出>, <错误>) 注入伪输出,defer 清理,然后断言结构化返回值。
  2. 安装/检测逻辑pkg/installerpkg/detector):用 mock.NewMockCommandExecutor() 构造执行器,SetCommandResult 预设返回,再走业务逻辑。
  3. HTTP 客户端pkg/client):用标准 httptest.NewServer 起伪 Packagist 服务,把客户端 base URL 指向它。
  4. 领域模型pkg/domain):纯结构体,直接构造实例做断言即可。

运行测试

bash
make test           # 全部测试
make test-race      # 竞态检测
make test-coverage  # 覆盖率报告

注意

Mock 输出是全局共享的,测试之间务必 defer ClearMockOutputsAdvanced() 清理,否则会污染其他用例。

基于 MIT 许可证发布