go-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWhen to Use
适用场景
Use this skill when:
- Writing Go unit tests
- Testing Bubbletea TUI components
- Creating table-driven tests
- Adding integration tests
- Using golden file testing
在以下场景使用该技能:
- 编写Go单元测试
- 测试Bubbletea TUI组件
- 创建表格驱动测试
- 添加集成测试
- 使用黄金文件测试
Critical Patterns
核心测试模式
Pattern 1: Table-Driven Tests
模式一:表格驱动测试
Standard Go pattern for multiple test cases:
go
func TestSomething(t *testing.T) {
tests := []struct {
name string
input string
expected string
wantErr bool
}{
{
name: "valid input",
input: "hello",
expected: "HELLO",
wantErr: false,
},
{
name: "empty input",
input: "",
expected: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ProcessInput(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
return
}
if result != tt.expected {
t.Errorf("got %q, want %q", result, tt.expected)
}
})
}
}适用于多测试用例的标准Go模式:
go
func TestSomething(t *testing.T) {
tests := []struct {
name string
input string
expected string
wantErr bool
}{
{
name: "valid input",
input: "hello",
expected: "HELLO",
wantErr: false,
},
{
name: "empty input",
input: "",
expected: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ProcessInput(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
return
}
if result != tt.expected {
t.Errorf("got %q, want %q", result, tt.expected)
}
})
}
}Pattern 2: Bubbletea Model Testing
模式二:Bubbletea Model测试
Test Model state transitions directly:
go
func TestModelUpdate(t *testing.T) {
m := NewModel()
// Simulate key press
newModel, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
m = newModel.(Model)
if m.Screen != ScreenMainMenu {
t.Errorf("expected ScreenMainMenu, got %v", m.Screen)
}
}直接测试Model状态转换:
go
func TestModelUpdate(t *testing.T) {
m := NewModel()
// 模拟按键
newModel, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
m = newModel.(Model)
if m.Screen != ScreenMainMenu {
t.Errorf("expected ScreenMainMenu, got %v", m.Screen)
}
}Pattern 3: Teatest Integration Tests
模式三:Teatest集成测试
Use Charmbracelet's teatest for TUI testing:
go
func TestInteractiveFlow(t *testing.T) {
m := NewModel()
tm := teatest.NewTestModel(t, m)
// Send keys
tm.Send(tea.KeyMsg{Type: tea.KeyEnter})
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
tm.Send(tea.KeyMsg{Type: tea.KeyEnter})
// Wait for model to update
tm.WaitFinished(t, teatest.WithDuration(time.Second))
// Get final model
finalModel := tm.FinalModel(t).(Model)
if finalModel.Screen != ExpectedScreen {
t.Errorf("wrong screen: got %v", finalModel.Screen)
}
}使用Charmbracelet的teatest进行TUI测试:
go
func TestInteractiveFlow(t *testing.T) {
m := NewModel()
tm := teatest.NewTestModel(t, m)
// 发送按键
tm.Send(tea.KeyMsg{Type: tea.KeyEnter})
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
tm.Send(tea.KeyMsg{Type: tea.KeyEnter})
// 等待Model更新
tm.WaitFinished(t, teatest.WithDuration(time.Second))
// 获取最终Model
finalModel := tm.FinalModel(t).(Model)
if finalModel.Screen != ExpectedScreen {
t.Errorf("wrong screen: got %v", finalModel.Screen)
}
}Pattern 4: Golden File Testing
模式四:黄金文件测试
Compare output against saved "golden" files:
go
func TestOSSelectGolden(t *testing.T) {
m := NewModel()
m.Screen = ScreenOSSelect
m.Width = 80
m.Height = 24
output := m.View()
golden := filepath.Join("testdata", "TestOSSelectGolden.golden")
if *update {
os.WriteFile(golden, []byte(output), 0644)
}
expected, _ := os.ReadFile(golden)
if output != string(expected) {
t.Errorf("output doesn't match golden file")
}
}将输出与已保存的「黄金文件」对比:
go
func TestOSSelectGolden(t *testing.T) {
m := NewModel()
m.Screen = ScreenOSSelect
m.Width = 80
m.Height = 24
output := m.View()
golden := filepath.Join("testdata", "TestOSSelectGolden.golden")
if *update {
os.WriteFile(golden, []byte(output), 0644)
}
expected, _ := os.ReadFile(golden)
if output != string(expected) {
t.Errorf("output doesn't match golden file")
}
}Decision Tree
决策树
Testing a function?
├── Pure function? → Table-driven test
├── Has side effects? → Mock dependencies
├── Returns error? → Test both success and error cases
└── Complex logic? → Break into smaller testable units
Testing TUI component?
├── State change? → Test Model.Update() directly
├── Full flow? → Use teatest.NewTestModel()
├── Visual output? → Use golden file testing
└── Key handling? → Send tea.KeyMsg
Testing system/exec?
├── Mock os/exec? → Use interface + mock
├── Real commands? → Integration test with --short skip
└── File operations? → Use t.TempDir()测试函数?
├── 纯函数? → 表格驱动测试
├── 有副作用? → 模拟依赖
├── 返回错误? → 测试成功与错误两种情况
└── 逻辑复杂? → 拆分为更小的可测试单元
测试TUI组件?
├── 状态变化? → 直接测试Model.Update()
├── 完整流程? → 使用teatest.NewTestModel()
├── 视觉输出? → 使用黄金文件测试
└── 按键处理? → 发送tea.KeyMsg
测试系统/exec?
├── 模拟os/exec? → 使用接口+模拟
├── 真实命令? → 集成测试,通过--short跳过
└── 文件操作? → 使用t.TempDir()Code Examples
代码示例
Example 1: Testing Key Navigation
示例一:测试按键导航
go
func TestCursorNavigation(t *testing.T) {
tests := []struct {
name string
startPos int
key string
endPos int
numOptions int
}{
{"down from 0", 0, "j", 1, 5},
{"up from 1", 1, "k", 0, 5},
{"down at bottom", 4, "j", 4, 5}, // stays at bottom
{"up at top", 0, "k", 0, 5}, // stays at top
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewModel()
m.Cursor = tt.startPos
// Set up options...
newModel, _ := m.Update(tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune(tt.key),
})
m = newModel.(Model)
if m.Cursor != tt.endPos {
t.Errorf("cursor = %d, want %d", m.Cursor, tt.endPos)
}
})
}
}go
func TestCursorNavigation(t *testing.T) {
tests := []struct {
name string
startPos int
key string
endPos int
numOptions int
}{
{"down from 0", 0, "j", 1, 5},
{"up from 1", 1, "k", 0, 5},
{"down at bottom", 4, "j", 4, 5}, // 停在底部
{"up at top", 0, "k", 0, 5}, // 停在顶部
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewModel()
m.Cursor = tt.startPos
// 设置选项...
newModel, _ := m.Update(tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune(tt.key),
})
m = newModel.(Model)
if m.Cursor != tt.endPos {
t.Errorf("cursor = %d, want %d", m.Cursor, tt.endPos)
}
})
}
}Example 2: Testing Screen Transitions
示例二:测试页面切换
go
func TestScreenTransitions(t *testing.T) {
tests := []struct {
name string
startScreen Screen
action tea.Msg
expectScreen Screen
}{
{
name: "welcome to main menu",
startScreen: ScreenWelcome,
action: tea.KeyMsg{Type: tea.KeyEnter},
expectScreen: ScreenMainMenu,
},
{
name: "escape from OS select",
startScreen: ScreenOSSelect,
action: tea.KeyMsg{Type: tea.KeyEsc},
expectScreen: ScreenMainMenu,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewModel()
m.Screen = tt.startScreen
newModel, _ := m.Update(tt.action)
m = newModel.(Model)
if m.Screen != tt.expectScreen {
t.Errorf("screen = %v, want %v", m.Screen, tt.expectScreen)
}
})
}
}go
func TestScreenTransitions(t *testing.T) {
tests := []struct {
name string
startScreen Screen
action tea.Msg
expectScreen Screen
}{
{
name: "welcome to main menu",
startScreen: ScreenWelcome,
action: tea.KeyMsg{Type: tea.KeyEnter},
expectScreen: ScreenMainMenu,
},
{
name: "escape from OS select",
startScreen: ScreenOSSelect,
action: tea.KeyMsg{Type: tea.KeyEsc},
expectScreen: ScreenMainMenu,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewModel()
m.Screen = tt.startScreen
newModel, _ := m.Update(tt.action)
m = newModel.(Model)
if m.Screen != tt.expectScreen {
t.Errorf("screen = %v, want %v", m.Screen, tt.expectScreen)
}
})
}
}Example 3: Testing Trainer Exercises
示例三:测试训练器练习
go
func TestExerciseValidation(t *testing.T) {
exercise := &Exercise{
Solutions: []string{"w", "W", "e"},
Optimal: "w",
}
tests := []struct {
input string
valid bool
optimal bool
}{
{"w", true, true},
{"W", true, false},
{"e", true, false},
{"x", false, false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
valid := ValidateAnswer(exercise, tt.input)
optimal := IsOptimalAnswer(exercise, tt.input)
if valid != tt.valid {
t.Errorf("valid = %v, want %v", valid, tt.valid)
}
if optimal != tt.optimal {
t.Errorf("optimal = %v, want %v", optimal, tt.optimal)
}
})
}
}go
func TestExerciseValidation(t *testing.T) {
exercise := &Exercise{
Solutions: []string{"w", "W", "e"},
Optimal: "w",
}
tests := []struct {
input string
valid bool
optimal bool
}{
{"w", true, true},
{"W", true, false},
{"e", true, false},
{"x", false, false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
valid := ValidateAnswer(exercise, tt.input)
optimal := IsOptimalAnswer(exercise, tt.input)
if valid != tt.valid {
t.Errorf("valid = %v, want %v", valid, tt.valid)
}
if optimal != tt.optimal {
t.Errorf("optimal = %v, want %v", optimal, tt.optimal)
}
})
}
}Example 4: Mocking System Info
示例四:模拟系统信息
go
func TestWithMockedSystem(t *testing.T) {
m := NewModel()
// Mock system info for testing
m.SystemInfo = &system.SystemInfo{
OS: system.OSMac,
IsARM: true,
HasBrew: true,
HomeDir: t.TempDir(),
}
// Now test with controlled environment
m.SetupInstallSteps()
// Verify expected steps
hasHomebrew := false
for _, step := range m.Steps {
if step.ID == "homebrew" {
hasHomebrew = true
}
}
if hasHomebrew {
t.Error("should not have homebrew step when HasBrew=true")
}
}go
func TestWithMockedSystem(t *testing.T) {
m := NewModel()
// 模拟系统信息用于测试
m.SystemInfo = &system.SystemInfo{
OS: system.OSMac,
IsARM: true,
HasBrew: true,
HomeDir: t.TempDir(),
}
// 在受控环境中测试
m.SetupInstallSteps()
// 验证预期步骤
hasHomebrew := false
for _, step := range m.Steps {
if step.ID == "homebrew" {
hasHomebrew = true
}
}
if hasHomebrew {
t.Error("should not have homebrew step when HasBrew=true")
}
}Test File Organization
测试文件组织结构
installer/internal/tui/
├── model.go
├── model_test.go # Model tests
├── update.go
├── update_test.go # Update handler tests
├── view.go
├── view_test.go # View rendering tests
├── teatest_test.go # Teatest integration tests
├── comprehensive_test.go # Full flow tests
├── testdata/
│ ├── TestOSSelectGolden.golden
│ └── TestViewGolden.golden
└── trainer/
├── types.go
├── types_test.go
├── exercises.go
├── exercises_test.go
└── simulator_test.goinstaller/internal/tui/
├── model.go
├── model_test.go # Model测试
├── update.go
├── update_test.go # 更新处理器测试
├── view.go
├── view_test.go # 视图渲染测试
├── teatest_test.go # Teatest集成测试
├── comprehensive_test.go # 全流程测试
├── testdata/
│ ├── TestOSSelectGolden.golden
│ └── TestViewGolden.golden
└── trainer/
├── types.go
├── types_test.go
├── exercises.go
├── exercises_test.go
└── simulator_test.goCommands
常用命令
bash
go test ./... # Run all tests
go test -v ./internal/tui/... # Verbose TUI tests
go test -run TestNavigation # Run specific test
go test -cover ./... # With coverage
go test -update ./... # Update golden files
go test -short ./... # Skip integration testsbash
go test ./... # 运行所有测试
go test -v ./internal/tui/... # 详细输出TUI测试
go test -run TestNavigation # 运行指定测试
go test -cover ./... # 生成测试覆盖率报告
go test -update ./... # 更新黄金文件
go test -short ./... # 跳过集成测试Resources
参考资源
- TUI Tests: See
installer/internal/tui/*_test.go - Trainer Tests: See
installer/internal/tui/trainer/*_test.go - System Tests: See
installer/internal/system/*_test.go - Golden Files: See
installer/internal/tui/testdata/ - Teatest Docs: https://github.com/charmbracelet/bubbletea/tree/master/teatest
- TUI测试: 查看
installer/internal/tui/*_test.go - 训练器测试: 查看
installer/internal/tui/trainer/*_test.go - 系统测试: 查看
installer/internal/system/*_test.go - 黄金文件: 查看
installer/internal/tui/testdata/ - Teatest文档: https://github.com/charmbracelet/bubbletea/tree/master/teatest