xsql 项目采用三层测试策略:单元测试、集成测试、端到端测试(E2E)。
| 层次 | 位置 | Build Tag | 依赖 | 运行速度 |
|---|---|---|---|---|
| 单元测试 | internal/*/、cmd/xsql/ |
无 | 无外部依赖 | 快(秒级) |
| 集成测试 | tests/integration/ |
integration |
MySQL + PostgreSQL | 中(10-30秒) |
| E2E 测试 | tests/e2e/ |
e2e |
无(或可选数据库) | 中(数秒) |
# 运行所有单元测试
go test ./...
# 运行集成测试(需要数据库)
go test -tags=integration ./tests/integration/...
# 运行 E2E 测试
go test -tags=e2e ./tests/e2e/...
# 全部测试(带覆盖率)
go test -cover ./...- 测试单个函数/方法的逻辑正确性
- 不依赖外部资源(数据库、文件系统、网络)
- 快速、隔离、可重复
测试文件放在被测代码同目录,命名为 *_test.go:
internal/config/
├── loader.go
├── loader_test.go # 单元测试
├── types.go
└── types_test.go
func TestParseDBType(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{"mysql", "mysql", "mysql", false},
{"pg", "pg", "pg", false},
{"postgres alias", "postgres", "pg", false},
{"invalid", "oracle", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseDBType(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}func assertOK(t *testing.T, resp Response) {
t.Helper() // 错误定位到调用处
if !resp.OK {
t.Errorf("expected ok=true, got false")
}
}func TestLoadConfig(t *testing.T) {
tmpDir := t.TempDir() // 测试结束自动清理
configPath := filepath.Join(tmpDir, "xsql.yaml")
// ...
}- 不要修改全局变量
- 使用依赖注入便于 mock
单元测试应覆盖:
- 配置优先级合并(CLI > ENV > Config)
- DSN/URL 解析
- 只读 SQL 判定(允许/拒绝用例)
- 输出序列化(json/yaml/csv/table)
- 错误码映射
- 测试多个模块协作
- 依赖真实数据库
- 验证 SQL 执行、只读策略等端到端逻辑
tests/integration/
├── cli_test.go # CLI 基础测试
├── db_test.go # 数据库连接测试
├── query_test.go # 查询相关测试
└── secret_test.go # 密钥管理测试
# 使用 docker-compose
docker-compose up -d
# 等待就绪
docker-compose ps# Linux/macOS
XSQL_TEST_MYSQL_DSN="root:root@tcp(127.0.0.1:3306)/testdb?parseTime=true" \
XSQL_TEST_PG_DSN="postgres://postgres:postgres@127.0.0.1:5432/testdb?sslmode=disable" \
go test -v -tags=integration ./tests/integration/...
# Windows PowerShell
$env:XSQL_TEST_MYSQL_DSN="root:root@tcp(127.0.0.1:3306)/testdb?parseTime=true"
$env:XSQL_TEST_PG_DSN="postgres://postgres:postgres@127.0.0.1:5432/testdb?sslmode=disable"
go test -v -tags=integration ./tests/integration/...//go:build integration
package integrationfunc TestMySQL_Query(t *testing.T) {
dsn := os.Getenv("XSQL_TEST_MYSQL_DSN")
if dsn == "" {
t.Skip("XSQL_TEST_MYSQL_DSN not set")
}
// ...
}var testBinary string
func TestMain(m *testing.M) {
tmpDir, _ := os.MkdirTemp("", "xsql-test")
defer os.RemoveAll(tmpDir)
testBinary = filepath.Join(tmpDir, "xsql")
cmd := exec.Command("go", "build", "-o", testBinary, "../../cmd/xsql")
if out, err := cmd.CombinedOutput(); err != nil {
panic(string(out))
}
os.Exit(m.Run())
}- 使用事务 + 回滚
- 或使用临时表/数据库
- MySQL/PostgreSQL 基本查询
- 多种数据类型处理
- 只读策略拦截(写操作被阻止)
- 连接错误处理
- 各种输出格式
SSH 客户端单元测试位于 internal/ssh/client_test.go,测试:
- 路径扩展(tilde expansion)
- 认证方法构建(私钥、默认密钥查找)
- known_hosts 校验
- SSH dial 失败错误码返回
- Passphrase 保护的密钥(正确/错误 passphrase)
# 运行 SSH 单元测试
go test -v ./internal/ssh/...测试 SSH CLI flags 与配置文件的合并行为:
--ssh-skip-known-hosts-check--ssh-identity-file--ssh-user--ssh-host
# 运行 SSH CLI flag 测试
go test -tags=e2e -v -run "SSH" ./tests/e2e/...需要真实 SSH 服务器的测试(跳过如果环境未配置):
ssh_proxy_success_test.go
设置环境变量:
export SSH_TEST_HOST=your-ssh-server
export SSH_TEST_PORT=22
export SSH_TEST_USER=your-user
export SSH_TEST_KEY_PATH=/path/to/private/key
export SSH_KNOWN_HOSTS_FILE=/path/to/known_hosts # 可选
# 可选:MySQL/PG over SSH
export XSQL_TEST_MYSQL_DSN="..."
export XSQL_TEST_PG_DSN="..."- SSH 代理测试需要可访问的 SSH 服务器
- 使用
SkipKnownHostsCheck: true进行测试,避免 known_hosts 问题 - SSH 密钥应有适当的权限(600 或 400)
- 黑盒测试:通过 CLI 接口测试整个系统
- 验证用户场景
- 不关心内部实现
tests/e2e/
├── e2e_test.go # 共享 helper 和基础设施
├── mcp_test.go # MCP Server 测试
├── output_test.go # 输出格式测试
├── profile_test.go # profile 命令测试
├── proxy_test.go # proxy 命令测试
├── readonly_test.go # 只读策略测试
├── ssh_cli_flags_test.go # SSH CLI flags 测试
└── ssh_proxy_success_test.go # SSH 代理成功测试(需要真实 SSH)
# 不需要数据库的测试
go test -tags=e2e ./tests/e2e/...
# 需要数据库的测试(设置 DSN 环境变量)
XSQL_TEST_MYSQL_DSN="..." go test -tags=e2e ./tests/e2e/...func runXSQL(t *testing.T, args ...string) (stdout, stderr string, exitCode int) {
t.Helper()
cmd := exec.Command(testBinary, args...)
var outBuf, errBuf bytes.Buffer
cmd.Stdout = &outBuf
cmd.Stderr = &errBuf
err := cmd.Run()
exitCode = 0
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
}
return outBuf.String(), errBuf.String(), exitCode
}func createTempConfig(t *testing.T, content string) string {
t.Helper()
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "xsql.yaml")
os.WriteFile(configPath, []byte(content), 0600)
return configPath
}func TestProfile_List_JSON(t *testing.T) {
config := createTempConfig(t, `profiles:
dev:
description: "开发环境"
db: mysql
`)
stdout, _, exitCode := runXSQL(t, "profile", "list", "--config", config, "--format", "json")
if exitCode != 0 {
t.Fatalf("exit code %d", exitCode)
}
var resp struct {
OK bool `json:"ok"`
Data struct {
Profiles []struct {
Name string `json:"name"`
Description string `json:"description"`
} `json:"profiles"`
} `json:"data"`
}
json.Unmarshal([]byte(stdout), &resp)
if !resp.OK {
t.Error("expected ok=true")
}
}func TestProfile_List_Table(t *testing.T) {
// ...
stdout, _, _ := runXSQL(t, "profile", "list", "--format", "table")
// 验证表头
if !strings.Contains(stdout, "NAME") {
t.Error("missing NAME header")
}
if !strings.Contains(stdout, "DESCRIPTION") {
t.Error("missing DESCRIPTION header")
}
}func TestProfile_Show_NotFound(t *testing.T) {
config := createTempConfig(t, `profiles: {}`)
_, _, exitCode := runXSQL(t, "profile", "show", "nonexistent", "--config", config)
if exitCode != 2 { // XSQL_CFG_INVALID
t.Errorf("expected exit code 2, got %d", exitCode)
}
}- 所有命令的 JSON/YAML/Table/CSV 输出
- 错误场景的退出码
- 密码脱敏
- Unicode/特殊字符处理
- 配置文件不存在等边界情况
Test<Module>_<Function>_<Scenario>
示例:
- TestConfig_Load_Priority # 配置加载优先级
- TestQuery_MySQL_ReadOnly # MySQL 只读查询
- TestProfile_List_JSON # profile list 的 JSON 输出
- TestProfile_Show_NotFound # profile show 找不到
GitHub Actions 自动运行:
# 单元测试(所有 PR)
- run: go test ./...
# 集成测试(需要数据库服务)
- run: go test -tags=integration ./tests/integration/...
# E2E 测试
- run: go test -tags=e2e ./tests/e2e/...详见 .github/workflows/ci.yml。
- 单元测试:在
internal/*/添加核心逻辑测试 - E2E 测试:在
tests/e2e/验证 CLI 输出(JSON + Table) - 集成测试(如涉及数据库):在
tests/integration/验证端到端 - 更新文档:确保
docs/cli-spec.md的输出示例与实际一致