Testing Utilities Reference
Overview
go-tui ships two test doubles that let you verify component behavior without a real terminal: MockTerminal and MockEventReader. They replace the two runtime dependencies every go-tui app needs: a screen to draw on and a source of user input.
MockTerminal implements the full Terminal interface. It keeps an in-memory cell grid you can inspect after rendering. MockEventReader implements both EventReader and InterruptibleReader, returning pre-queued events one at a time so your tests can simulate any input sequence deterministically.
import tui "github.com/grindlemire/go-tui"
term := tui.NewMockTerminal(80, 24)
reader := tui.NewMockEventReader(
tui.KeyEvent{Key: tui.KeyRune, Rune: 'q'},
)
MockTerminal
A simulated terminal backed by a flat cell buffer. All Terminal methods operate in memory instead of touching the real TTY, so you can inspect cursor movements, mode transitions, and cell writes after the fact.
NewMockTerminal
func NewMockTerminal(width, height int) *MockTerminal
Creates a mock terminal with the given dimensions. All cells start as spaces with default styling. The default capabilities are:
| Field | Default |
|---|---|
| Colors | Color256 |
| Unicode | true |
| TrueColor | true |
| AltScreen | true |
term := tui.NewMockTerminal(80, 24)
w, h := term.Size() // 80, 24
Terminal Interface Methods
MockTerminal implements every method on the Terminal interface:
Size
func (m *MockTerminal) Size() (width, height int)
Returns the current dimensions. Changes after calling Resize.
Flush
func (m *MockTerminal) Flush(changes []CellChange)
Applies cell changes to the internal buffer. Out-of-bounds changes are silently ignored.
term := tui.NewMockTerminal(80, 24)
term.Flush([]tui.CellChange{
{X: 0, Y: 0, Cell: tui.NewCell('H', tui.NewStyle())},
{X: 1, Y: 0, Cell: tui.NewCell('i', tui.NewStyle())},
})
// term now has "Hi" at the top-left
Clear
func (m *MockTerminal) Clear()
Resets every cell to a space with default styling and moves the cursor to (0, 0).
ClearToEnd
func (m *MockTerminal) ClearToEnd()
Clears from the current cursor position to the end of the screen.
SetCursor, HideCursor, ShowCursor
func (m *MockTerminal) SetCursor(x, y int)
func (m *MockTerminal) HideCursor()
func (m *MockTerminal) ShowCursor()
Track cursor position and visibility. Query with Cursor() and IsCursorHidden().
EnterRawMode, ExitRawMode
func (m *MockTerminal) EnterRawMode() error
func (m *MockTerminal) ExitRawMode() error
Toggle raw mode state. Always return nil. Query with IsInRawMode().
EnterAltScreen, ExitAltScreen
func (m *MockTerminal) EnterAltScreen()
func (m *MockTerminal) ExitAltScreen()
Toggle alternate screen state. Each call increments the corresponding transition counter. Query with IsInAltScreen(), AltScreenEnterCount(), and AltScreenExitCount().
EnableMouse, DisableMouse
func (m *MockTerminal) EnableMouse()
func (m *MockTerminal) DisableMouse()
Toggle mouse reporting state. Query with IsMouseEnabled().
Caps
func (m *MockTerminal) Caps() Capabilities
Returns the current capabilities. Change them with SetCaps().
WriteDirect
func (m *MockTerminal) WriteDirect(b []byte) (int, error)
No-op. Returns len(b), nil. Raw escape sequences aren't processed in the mock.
Test Helper Methods
These methods exist only on MockTerminal (not on the Terminal interface) and are for assertions in test code.
CellAt
func (m *MockTerminal) CellAt(x, y int) Cell
Returns the Cell at the given position. Returns an empty Cell (zero rune, zero width) if the coordinates are out of bounds.
term := tui.NewMockTerminal(80, 24)
buf := tui.NewBuffer(80, 24)
buf.SetString(0, 0, "Hello", tui.NewStyle())
tui.Render(term, buf)
cell := term.CellAt(0, 0)
fmt.Println(cell.Rune) // 'H'
fmt.Println(cell.Style.HasAttr(tui.AttrBold)) // false
String
func (m *MockTerminal) String() string
Renders the entire buffer to a string. Each row becomes a line separated by \n. Continuation cells from wide characters are skipped. The result has no trailing newline after the last row.
term := tui.NewMockTerminal(5, 2)
term.Flush([]tui.CellChange{
{X: 0, Y: 0, Cell: tui.NewCell('A', tui.NewStyle())},
{X: 1, Y: 0, Cell: tui.NewCell('B', tui.NewStyle())},
})
fmt.Println(term.String())
// AB
//
StringTrimmed
func (m *MockTerminal) StringTrimmed() string
Like String, but trailing spaces are removed from each line. Usually the better choice for assertions since it ignores empty space that would otherwise cause false mismatches.
term := tui.NewMockTerminal(10, 3)
term.Flush([]tui.CellChange{
{X: 0, Y: 0, Cell: tui.NewCell('H', tui.NewStyle())},
{X: 1, Y: 0, Cell: tui.NewCell('i', tui.NewStyle())},
{X: 2, Y: 1, Cell: tui.NewCell('X', tui.NewStyle())},
})
fmt.Println(term.StringTrimmed())
// Hi
// X
//
Cursor
func (m *MockTerminal) Cursor() (x, y int)
Returns the current cursor position.
IsCursorHidden
func (m *MockTerminal) IsCursorHidden() bool
Returns true if HideCursor() was called without a subsequent ShowCursor().
IsInRawMode
func (m *MockTerminal) IsInRawMode() bool
Returns true if EnterRawMode() was called without a subsequent ExitRawMode().
IsInAltScreen
func (m *MockTerminal) IsInAltScreen() bool
Returns true if the terminal is currently in alternate screen mode.
AltScreenEnterCount
func (m *MockTerminal) AltScreenEnterCount() int
Returns the total number of times EnterAltScreen() has been called since creation or the last Reset(). Useful for verifying that a component enters the alternate screen exactly once.
AltScreenExitCount
func (m *MockTerminal) AltScreenExitCount() int
Returns the total number of times ExitAltScreen() has been called since creation or the last Reset().
IsMouseEnabled
func (m *MockTerminal) IsMouseEnabled() bool
Returns true if EnableMouse() was called without a subsequent DisableMouse().
SetCaps
func (m *MockTerminal) SetCaps(caps Capabilities)
Overrides the terminal's capabilities. Use this to test how your components behave under different terminal configurations.
term := tui.NewMockTerminal(80, 24)
// Simulate a 16-color terminal
term.SetCaps(tui.Capabilities{
Colors: tui.Color16,
Unicode: false,
TrueColor: false,
AltScreen: false,
})
caps := term.Caps()
fmt.Println(caps.Colors) // Color16
Reset
func (m *MockTerminal) Reset()
Returns the mock terminal to its initial state: clears all cells, resets the cursor to (0, 0), shows the cursor, exits raw mode, exits alternate screen, disables mouse, and zeros out the transition counters.
term := tui.NewMockTerminal(80, 24)
term.HideCursor()
term.EnterAltScreen()
term.Reset()
fmt.Println(term.IsCursorHidden()) // false
fmt.Println(term.IsInAltScreen()) // false
fmt.Println(term.AltScreenEnterCount()) // 0
Resize
func (m *MockTerminal) Resize(width, height int)
Changes the terminal dimensions. Content that falls within both the old and new bounds is preserved. Newly exposed cells are filled with spaces.
term := tui.NewMockTerminal(5, 5)
term.Flush([]tui.CellChange{
{X: 2, Y: 2, Cell: tui.NewCell('X', tui.NewStyle())},
})
term.Resize(10, 10)
w, h := term.Size() // 10, 10
fmt.Println(term.CellAt(2, 2).Rune) // 'X' — preserved
SetCell
func (m *MockTerminal) SetCell(x, y int, c Cell)
Directly writes a cell into the buffer. Out-of-bounds coordinates are silently ignored. Useful for setting up initial terminal state before running a test.
term := tui.NewMockTerminal(80, 24)
term.SetCell(0, 0, tui.NewCell('Z', tui.NewStyle().Bold()))
fmt.Println(term.CellAt(0, 0).Rune) // 'Z'
MockEventReader
A deterministic event source for testing. You pre-load it with events and they come back in order, one per PollEvent call.
NewMockEventReader
func NewMockEventReader(events ...Event) *MockEventReader
Creates a reader pre-loaded with the given events. The events are returned in order by successive calls to PollEvent. When the queue is exhausted, PollEvent returns (nil, false).
reader := tui.NewMockEventReader(
tui.KeyEvent{Key: tui.KeyRune, Rune: 'h'},
tui.KeyEvent{Key: tui.KeyRune, Rune: 'i'},
tui.KeyEvent{Key: tui.KeyEnter},
)
PollEvent
func (m *MockEventReader) PollEvent(timeout time.Duration) (Event, bool)
Returns the next queued event. The timeout parameter is ignored. Events are returned immediately. When all events have been consumed, returns (nil, false).
reader := tui.NewMockEventReader(
tui.KeyEvent{Key: tui.KeyEscape},
)
ev, ok := reader.PollEvent(0) // KeyEvent{Key: KeyEscape}, true
ev, ok = reader.PollEvent(0) // nil, false
Close
func (m *MockEventReader) Close() error
No-op. Always returns nil.
AddEvents
func (m *MockEventReader) AddEvents(events ...Event)
Appends more events to the end of the queue. Call this mid-test to simulate additional user input arriving after the initial batch.
reader := tui.NewMockEventReader(
tui.KeyEvent{Key: tui.KeyRune, Rune: 'a'},
)
reader.AddEvents(
tui.KeyEvent{Key: tui.KeyRune, Rune: 'b'},
tui.KeyEvent{Key: tui.KeyEnter},
)
fmt.Println(reader.Remaining()) // 3
Remaining
func (m *MockEventReader) Remaining() int
Returns the number of events still in the queue that haven't been consumed by PollEvent.
Reset
func (m *MockEventReader) Reset()
Rewinds the reader to the beginning so all originally queued events (plus any added with AddEvents) are returned again from the start.
reader := tui.NewMockEventReader(
tui.KeyEvent{Key: tui.KeyRune, Rune: 'x'},
)
reader.PollEvent(0) // consumes 'x'
reader.Reset()
ev, _ := reader.PollEvent(0)
fmt.Println(ev.(tui.KeyEvent).Rune) // 'x'
EnableInterrupt
func (m *MockEventReader) EnableInterrupt() error
No-op. Always returns nil. Exists to satisfy the InterruptibleReader interface.
Interrupt
func (m *MockEventReader) Interrupt() error
No-op. Always returns nil. Exists to satisfy the InterruptibleReader interface.
Common Testing Patterns
Render and Assert
Create a buffer, render content into it, flush to a mock terminal, and check the result.
func TestGreeting(t *testing.T) {
buf := tui.NewBuffer(20, 5)
term := tui.NewMockTerminal(20, 5)
buf.SetString(0, 0, "Hello", tui.NewStyle())
tui.Render(term, buf)
for i, r := range "Hello" {
cell := term.CellAt(i, 0)
if cell.Rune != r {
t.Errorf("CellAt(%d, 0).Rune = %q, want %q", i, cell.Rune, r)
}
}
}
Check Styled Content
Verify both the characters and their styling after rendering.
func TestStyledText(t *testing.T) {
buf := tui.NewBuffer(20, 5)
term := tui.NewMockTerminal(20, 5)
style := tui.NewStyle().Bold().Foreground(tui.Red)
buf.SetString(3, 2, "Alert", style)
tui.Render(term, buf)
cell := term.CellAt(3, 2)
if cell.Rune != 'A' {
t.Errorf("Rune = %q, want 'A'", cell.Rune)
}
if !cell.Style.HasAttr(tui.AttrBold) {
t.Error("expected bold text")
}
if !cell.Style.Fg.Equal(tui.Red) {
t.Error("expected red foreground")
}
}
Table-Driven Tests
go-tui follows a consistent table-driven test convention. Define the test case struct separately, use a map[string]tc for the cases, and iterate with t.Run.
func TestMockTerminal_Size(t *testing.T) {
type tc struct {
width, height int
}
tests := map[string]tc{
"standard 80x24": {width: 80, height: 24},
"large terminal": {width: 200, height: 60},
"small terminal": {width: 40, height: 10},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
m := tui.NewMockTerminal(tt.width, tt.height)
w, h := m.Size()
if w != tt.width || h != tt.height {
t.Errorf("Size() = (%d, %d), want (%d, %d)", w, h, tt.width, tt.height)
}
})
}
}
Testing with Borders
Verify that border drawing produces the expected box characters.
func TestBorderedBox(t *testing.T) {
buf := tui.NewBuffer(20, 6)
term := tui.NewMockTerminal(20, 6)
tui.DrawBox(buf, tui.NewRect(2, 1, 15, 4), tui.BorderSingle, tui.NewStyle())
tui.Render(term, buf)
if term.CellAt(2, 1).Rune != '┌' {
t.Errorf("top-left = %q, want '┌'", term.CellAt(2, 1).Rune)
}
if term.CellAt(16, 1).Rune != '┐' {
t.Errorf("top-right = %q, want '┐'", term.CellAt(16, 1).Rune)
}
if term.CellAt(2, 4).Rune != '└' {
t.Errorf("bottom-left = %q, want '└'", term.CellAt(2, 4).Rune)
}
if term.CellAt(16, 4).Rune != '┘' {
t.Errorf("bottom-right = %q, want '┘'", term.CellAt(16, 4).Rune)
}
}
Simulating Key Events
Create an app with a mock event reader to test how your component reacts to keyboard input.
reader := tui.NewMockEventReader(
tui.KeyEvent{Key: tui.KeyEnter},
)
app, err := tui.NewAppWithReader(reader,
tui.WithRootComponent(MyApp()),
)
if err != nil {
t.Fatal(err)
}
Testing Terminal Capabilities
Swap out the default capabilities to verify your component behaves correctly on limited terminals.
func TestLimitedTerminal(t *testing.T) {
term := tui.NewMockTerminal(80, 24)
term.SetCaps(tui.Capabilities{
Colors: tui.Color16,
Unicode: false,
TrueColor: false,
AltScreen: false,
})
caps := term.Caps()
if caps.TrueColor {
t.Error("expected no true color support")
}
}
Wide Character Support
Test CJK and emoji characters that occupy two cells.
func TestWideCharacter(t *testing.T) {
term := tui.NewMockTerminal(10, 3)
term.Flush([]tui.CellChange{
{X: 0, Y: 0, Cell: tui.NewCellWithWidth('中', tui.NewStyle(), 2)},
{X: 1, Y: 0, Cell: tui.NewCellWithWidth(0, tui.NewStyle(), 0)}, // continuation
})
cell := term.CellAt(0, 0)
if cell.Rune != '中' {
t.Errorf("Rune = %q, want '中'", cell.Rune)
}
if cell.Width != 2 {
t.Errorf("Width = %d, want 2", cell.Width)
}
if !term.CellAt(1, 0).IsContinuation() {
t.Error("cell at (1,0) should be a continuation")
}
}
Related
- App Reference —
NewApp,NewAppWithReader, and app lifecycle - Events Reference —
KeyEvent,MouseEvent, and key constants - Buffer Reference —
Buffer,Cell, and rendering functions - Testing Guide — step-by-step guide to writing tests