Testing framework for Bubble Tea applications.
Write deterministic, fast tests for your TUI apps — no terminal required.
Testing Bubble Tea apps today means either:
- Manual testing (slow, error-prone, not CI-friendly)
- Writing brittle tests that call Update() manually and inspect model internals
- No snapshot testing, no input simulation, no standard assertions
func TestMyApp(t *testing.T) {
sim := charmtest.New(NewMyModel())
sim.Type("hello world") // simulate typing
sim.SendKey("enter") // simulate key press
charmtest.RequireViewContains(t, sim, "hello world") // assert on output
charmtest.RequireSnapshot(t, sim) // snapshot testing
}go get github.com/junhinhow/charm-testThe Simulator drives a Bubble Tea model synchronously — no goroutines, no terminal, fully deterministic.
sim := charmtest.New(myModel)
sim := charmtest.New(myModel, charmtest.WithSize(120, 40))sim.SendKey("enter") // named keys
sim.SendKey("j") // single characters
sim.SendKey("ctrl+c") // key combinations
sim.SendKeys("k", "k", "k") // multiple keys
sim.Type("hello") // type a string (char by char)
sim.Resize(100, 30) // simulate terminal resizeSupported keys: enter, esc, tab, shift+tab, backspace, delete, up, down, left, right, home, end, pgup, pgdown, space, ctrl+c, ctrl+a, ctrl+d, ctrl+e, ctrl+k, ctrl+u, ctrl+n, ctrl+p, and any single character.
charmtest.RequireView(t, sim, "exact match")
charmtest.RequireViewContains(t, sim, "substring")
charmtest.RequireViewNotContains(t, sim, "should not appear")
charmtest.RequireViewLines(t, sim, 5) // exactly 5 non-empty linesAll assertions strip ANSI escape sequences before comparison, so styled output is matched by content only.
charmtest.RequireSnapshot(t, sim) // auto-named by test
charmtest.RequireSnapshotNamed(t, sim, "after-login") // custom nameGolden files are stored in testdata/. On first run, the snapshot is created. On subsequent runs, the view is compared against the stored snapshot.
Update snapshots: CHARM_TEST_UPDATE=1 go test ./...
charmtest.DumpView(t, sim) // print current view to test log
charmtest.DumpMessages(t, sim) // print all processed messages
charmtest.ViewString(sim) // get ANSI-stripped view as stringmodel := sim.Model().(*MyModel) // type assert to your model
assert.Equal(t, 5, model.Count)See examples/counter/ for a complete working example with tests.
func TestCounter_Increment(t *testing.T) {
sim := charmtest.New(counter.New())
sim.SendKeys("k", "k", "k")
charmtest.RequireViewContains(t, sim, "Count: 3")
}
func TestCounter_Snapshot(t *testing.T) {
sim := charmtest.New(counter.New())
sim.SendKeys("k", "k", "k", "k", "k")
charmtest.RequireSnapshot(t, sim)
}- Deterministic: No goroutines, no timers — same input always produces same output
- Fast: Tests run in microseconds, not seconds
- Simple: One import, one constructor, intuitive API
- Idiomatic: Works with
testing.T, follows Go testing conventions - ANSI-aware: All comparisons strip escape sequences automatically
MIT