Testing
AliveJTUI is designed to be testable without a real terminal. The MockBackend provides a virtual 2D screen buffer and a key event queue, so you can write deterministic unit tests for your components.
MockBackend
MockBackend simulates a terminal of a given size. It captures rendered output in a cell buffer and lets you inject key events programmatically.
Basic Setup
import io.github.yehorsyrin.tui.backend.MockBackend;
import io.github.yehorsyrin.tui.core.AliveJTUI;
import io.github.yehorsyrin.tui.event.*;
MockBackend backend = new MockBackend(80, 24); // 80 cols × 24 rows
AliveJTUI.run(new MyApp(), backend);
Simulating Key Presses
// Named key
backend.sendKey(KeyEvent.of(KeyType.ARROW_DOWN));
backend.sendKey(KeyEvent.of(KeyType.ENTER));
backend.sendKey(KeyEvent.of(KeyType.ESCAPE));
// Printable character
backend.sendKey(KeyEvent.ofCharacter('x'));
backend.sendKey(KeyEvent.ofCharacter('A'));
Inspecting Rendered Output
// Character at column=0, row=0
String cell = backend.getCell(0, 0);
// Check a specific cell
assertEquals("H", backend.getCell(0, 1));
Writing Unit Tests
JUnit 5 Example
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CounterAppTest {
private MockBackend backend;
@BeforeEach
void setUp() {
backend = new MockBackend(80, 24);
AliveJTUI.run(new CounterApp(), backend);
}
@Test
void initialRenderShowsZero() {
// The counter starts at 0
// Find "0" somewhere in the rendered output
boolean found = false;
for (int col = 0; col < 80; col++) {
if ("0".equals(backend.getCell(col, 2))) {
found = true;
break;
}
}
assertTrue(found, "Expected '0' to appear in the initial render");
}
@Test
void arrowUpIncrementsCounter() {
backend.sendKey(KeyEvent.of(KeyType.ARROW_UP));
backend.sendKey(KeyEvent.of(KeyType.ARROW_UP));
backend.sendKey(KeyEvent.of(KeyType.ARROW_UP));
// Counter should now be 3
boolean found = false;
for (int col = 0; col < 80; col++) {
if ("3".equals(backend.getCell(col, 2))) {
found = true;
break;
}
}
assertTrue(found, "Expected '3' after three ARROW_UP presses");
}
@Test
void arrowDownDecrementsCounter() {
backend.sendKey(KeyEvent.of(KeyType.ARROW_DOWN));
boolean found = false;
for (int col = 0; col < 80; col++) {
if ("-1".equals(backend.getCell(col, 2))) {
found = true;
break;
}
}
assertTrue(found, "Expected '-1' after one ARROW_DOWN press");
}
}
Testing Patterns
Test State Changes via Key Events
The most reliable way to test a component is to simulate key presses and verify that the rendered output changes accordingly.
@Test
void checkboxToggles() {
MockBackend backend = new MockBackend(80, 24);
AliveJTUI.run(new SettingsApp(), backend);
// Verify initially unchecked: look for ☐
// Press X to toggle
backend.sendKey(KeyEvent.ofCharacter('x'));
// Now look for ☑
}
Test Async Operations
setStateAsync uses a background thread. In tests, you may need to wait for the state to settle:
@Test
void asyncDataLoads() throws InterruptedException {
MockBackend backend = new MockBackend(80, 24);
AliveJTUI.run(new DataApp(), backend);
// Trigger async load
backend.sendKey(KeyEvent.of(KeyType.ENTER));
// Give the background thread time to complete
Thread.sleep(500);
// Verify the data appeared in the render
// ...
}
Deterministic async tests
For more deterministic async tests, inject a fake data source via constructor or setter that returns immediately, rather than relying on Thread.sleep.
Test Focus Navigation
@Test
void tabCyclesFocus() {
MockBackend backend = new MockBackend(80, 24);
AliveJTUI.run(new FormApp(), backend);
// Press Tab to move focus
backend.sendKey(KeyEvent.of(KeyType.TAB));
// Verify the focused element changed
// (look for focus indicator character in the rendered output)
backend.sendKey(KeyEvent.of(KeyType.TAB));
// Second Tab moves to next focusable
backend.sendKey(KeyEvent.of(KeyType.SHIFT_TAB));
// Shift+Tab moves back
}
Test Dialog Interactions
@Test
void dialogConfirmationDeletesItem() {
MockBackend backend = new MockBackend(80, 24);
AliveJTUI.run(new ListApp(), backend);
// Open dialog with D
backend.sendKey(KeyEvent.ofCharacter('d'));
// Confirm with Enter (assumes [Yes] button is focused)
backend.sendKey(KeyEvent.of(KeyType.ENTER));
// Verify item was deleted — check rendered output
}
Available Backends
| Backend | Description |
|---|---|
LanternaBackend |
Default. Opens a Swing window when a display is available; falls back to in-terminal mode on headless Linux. |
MockBackend |
For unit testing. No real terminal required. Captures output in a cell buffer. |
| Custom | Implement TerminalBackend for any other rendering target. |
// Default
AliveJTUI.run(new MyApp());
// Testing
AliveJTUI.run(new MyApp(), new MockBackend(80, 24));
// Custom
AliveJTUI.run(new MyApp(), new MyCustomBackend());
Tips for Testable Components
Keep render() pure
render() should only read state fields — no I/O, no side effects. Pure render functions are easy to reason about in tests.
Inject dependencies
Pass services (data loaders, clocks, etc.) via constructor. Replace them with test doubles in unit tests.
Small components
Large monolithic components are harder to test. Break complex UIs into smaller components, each responsible for one part of the screen.
Test the component, not the pixels
Where possible, test behavior (state transitions, callback invocations) rather than exact pixel positions in the cell buffer.