🧪 Testability Design
The 586 test cases in Composer Skills (referred to as 450+ externally in the README) all pass on a machine with no PHP, no Composer, and no network, thanks to the Mock mechanism that runs through both the base layer and the SDK layer.
Test Scale Overview
| Package | Number of func Test |
|---|---|
pkg/composer | 422 |
pkg/installer | 78 |
pkg/client | 22 |
pkg/composerutils | 20 |
pkg/detector | 17 |
pkg/repository | 16 |
pkg/domain | 11 |
| Total | 586 |
🔌 Mock Mechanism 1: SetupMockOutput in pkg/composer
Defined in pkg/composer/composer.go and pkg/composer/test_utils.go.
Core idea: a package-level variable testMode marks the test state; in test state, validation of the real composer binary is skipped. Meanwhile, a global map[string]MockOutput injects fake output keyed by "command string".
// composer.go
var testMode = false
// Enable test mode and set mock output for a specific command
func SetupMockOutput(command string, output string, err error) {
// ... writes to the mockOutputs map ...
testMode = true
}// Usage in tests
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)
}Concurrency safety is guaranteed by a sync.RWMutex (mockMutex in test_utils.go).
🧩 Mock Mechanism 2: MockCommandExecutor in pkg/composerutils/mock
For scenarios that need to replace the command executor itself, defined in pkg/composerutils/mock/mock.go.
// CommandExecutor interface — a replaceable command executor
type CommandExecutor interface {
Execute(name string, args ...string) ([]byte, error)
}
// Default implementation: actually executes system commands
type DefaultCommandExecutor struct{}
// Mock implementation: returns preset results
type MockCommandExecutor struct {
CommandResults map[string]struct{ Output []byte; Err error }
}The 78 tests in pkg/installer inject a MockCommandExecutor to verify the "detected missing → triggers install" branch logic without actually downloading PHP.
exec := mock.NewMockCommandExecutor()
exec.SetCommandResult("composer", []string{"--version"},
[]byte("Composer version 2.7.0"), nil)⚙️ The Role of testMode
Around line 252 of composer.go: when testMode == false (non-test state), New() forcibly validates that the local composer binary exists; in test state this check is skipped so that tests do not depend on a real environment. SetupMockOutput / ClearMockOutputsAdvanced automatically set testMode to true.
🗂️ Test Organization
- Co-located: every
foo.gohas a neighboringfoo_test.go; 61 test files are spread across 7 packages. - Integration tests:
pkg/composer/integration_test.gochains multiple Mocks into a full workflow (detect → install → execute → audit). - Shared helpers: functions like
createTestComposerLockinpkg/composer/test_utils.goare reused across multiple test files, avoiding duplicate fixture boilerplate.
📝 How to Write Tests for Your Own Code
- CLI wrapper methods (
pkg/composer): callcomposer.SetupMockOutput("composer <subcommand> <args>", <output>, <error>)to inject fake output,defercleanup, then assert on the structured return value. - Install/detect logic (
pkg/installer,pkg/detector): build an executor withmock.NewMockCommandExecutor(), preset returns viaSetCommandResult, then run the business logic. - HTTP client (
pkg/client): stand up a fake Packagist server with the standardhttptest.NewServerand point the client's base URL at it. - Domain model (
pkg/domain): pure structs — just construct instances and assert directly.
Running Tests
make test # all tests
make test-race # race detection
make test-coverage # coverage reportNote
Mock output is globally shared; always defer ClearMockOutputsAdvanced() between tests, otherwise it will pollute other cases.