When 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
Critical Patterns
Pattern 1: Table-Driven Tests
Standard Go pattern for multiple test cases:
go
1func TestSomething(t *testing.T) {
2 tests := []struct {
3 name string
4 input string
5 expected string
6 wantErr bool
7 }{
8 {
9 name: "valid input",
10 input: "hello",
11 expected: "HELLO",
12 wantErr: false,
13 },
14 {
15 name: "empty input",
16 input: "",
17 expected: "",
18 wantErr: true,
19 },
20 }
21
22 for _, tt := range tests {
23 t.Run(tt.name, func(t *testing.T) {
24 result, err := ProcessInput(tt.input)
25
26 if (err != nil) != tt.wantErr {
27 t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
28 return
29 }
30
31 if result != tt.expected {
32 t.Errorf("got %q, want %q", result, tt.expected)
33 }
34 })
35 }
36}
Pattern 2: Bubbletea Model Testing
Test Model state transitions directly:
go
1func TestModelUpdate(t *testing.T) {
2 m := NewModel()
3
4 // Simulate key press
5 newModel, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
6 m = newModel.(Model)
7
8 if m.Screen != ScreenMainMenu {
9 t.Errorf("expected ScreenMainMenu, got %v", m.Screen)
10 }
11}
Pattern 3: Teatest Integration Tests
Use Charmbracelet's teatest for TUI testing:
go
1func TestInteractiveFlow(t *testing.T) {
2 m := NewModel()
3 tm := teatest.NewTestModel(t, m)
4
5 // Send keys
6 tm.Send(tea.KeyMsg{Type: tea.KeyEnter})
7 tm.Send(tea.KeyMsg{Type: tea.KeyDown})
8 tm.Send(tea.KeyMsg{Type: tea.KeyEnter})
9
10 // Wait for model to update
11 tm.WaitFinished(t, teatest.WithDuration(time.Second))
12
13 // Get final model
14 finalModel := tm.FinalModel(t).(Model)
15
16 if finalModel.Screen != ExpectedScreen {
17 t.Errorf("wrong screen: got %v", finalModel.Screen)
18 }
19}
Pattern 4: Golden File Testing
Compare output against saved "golden" files:
go
1func TestOSSelectGolden(t *testing.T) {
2 m := NewModel()
3 m.Screen = ScreenOSSelect
4 m.Width = 80
5 m.Height = 24
6
7 output := m.View()
8
9 golden := filepath.Join("testdata", "TestOSSelectGolden.golden")
10
11 if *update {
12 os.WriteFile(golden, []byte(output), 0644)
13 }
14
15 expected, _ := os.ReadFile(golden)
16 if output != string(expected) {
17 t.Errorf("output doesn't match golden file")
18 }
19}
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()
Code Examples
Example 1: Testing Key Navigation
go
1func TestCursorNavigation(t *testing.T) {
2 tests := []struct {
3 name string
4 startPos int
5 key string
6 endPos int
7 numOptions int
8 }{
9 {"down from 0", 0, "j", 1, 5},
10 {"up from 1", 1, "k", 0, 5},
11 {"down at bottom", 4, "j", 4, 5}, // stays at bottom
12 {"up at top", 0, "k", 0, 5}, // stays at top
13 }
14
15 for _, tt := range tests {
16 t.Run(tt.name, func(t *testing.T) {
17 m := NewModel()
18 m.Cursor = tt.startPos
19 // Set up options...
20
21 newModel, _ := m.Update(tea.KeyMsg{
22 Type: tea.KeyRunes,
23 Runes: []rune(tt.key),
24 })
25 m = newModel.(Model)
26
27 if m.Cursor != tt.endPos {
28 t.Errorf("cursor = %d, want %d", m.Cursor, tt.endPos)
29 }
30 })
31 }
32}
Example 2: Testing Screen Transitions
go
1func TestScreenTransitions(t *testing.T) {
2 tests := []struct {
3 name string
4 startScreen Screen
5 action tea.Msg
6 expectScreen Screen
7 }{
8 {
9 name: "welcome to main menu",
10 startScreen: ScreenWelcome,
11 action: tea.KeyMsg{Type: tea.KeyEnter},
12 expectScreen: ScreenMainMenu,
13 },
14 {
15 name: "escape from OS select",
16 startScreen: ScreenOSSelect,
17 action: tea.KeyMsg{Type: tea.KeyEsc},
18 expectScreen: ScreenMainMenu,
19 },
20 }
21
22 for _, tt := range tests {
23 t.Run(tt.name, func(t *testing.T) {
24 m := NewModel()
25 m.Screen = tt.startScreen
26
27 newModel, _ := m.Update(tt.action)
28 m = newModel.(Model)
29
30 if m.Screen != tt.expectScreen {
31 t.Errorf("screen = %v, want %v", m.Screen, tt.expectScreen)
32 }
33 })
34 }
35}
Example 3: Testing Trainer Exercises
go
1func TestExerciseValidation(t *testing.T) {
2 exercise := &Exercise{
3 Solutions: []string{"w", "W", "e"},
4 Optimal: "w",
5 }
6
7 tests := []struct {
8 input string
9 valid bool
10 optimal bool
11 }{
12 {"w", true, true},
13 {"W", true, false},
14 {"e", true, false},
15 {"x", false, false},
16 }
17
18 for _, tt := range tests {
19 t.Run(tt.input, func(t *testing.T) {
20 valid := ValidateAnswer(exercise, tt.input)
21 optimal := IsOptimalAnswer(exercise, tt.input)
22
23 if valid != tt.valid {
24 t.Errorf("valid = %v, want %v", valid, tt.valid)
25 }
26 if optimal != tt.optimal {
27 t.Errorf("optimal = %v, want %v", optimal, tt.optimal)
28 }
29 })
30 }
31}
Example 4: Mocking System Info
go
1func TestWithMockedSystem(t *testing.T) {
2 m := NewModel()
3
4 // Mock system info for testing
5 m.SystemInfo = &system.SystemInfo{
6 OS: system.OSMac,
7 IsARM: true,
8 HasBrew: true,
9 HomeDir: t.TempDir(),
10 }
11
12 // Now test with controlled environment
13 m.SetupInstallSteps()
14
15 // Verify expected steps
16 hasHomebrew := false
17 for _, step := range m.Steps {
18 if step.ID == "homebrew" {
19 hasHomebrew = true
20 }
21 }
22
23 if hasHomebrew {
24 t.Error("should not have homebrew step when HasBrew=true")
25 }
26}
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.go
Commands
bash
1go test ./... # Run all tests
2go test -v ./internal/tui/... # Verbose TUI tests
3go test -run TestNavigation # Run specific test
4go test -cover ./... # With coverage
5go test -update ./... # Update golden files
6go test -short ./... # Skip integration tests
Resources