Writing Tests
Test your Nocterm components
Nocterm provides a testing framework for TUI components. This guide shows you how to write tests.
Test setup
Import the test library:
import 'package:nocterm/nocterm.dart';
import 'package:nocterm/nocterm_test.dart';
import 'package:test/test.dart';
void main() {
test('counter increments', () async {
await testNocterm(
'counter test',
(tester) async {
await tester.pumpComponent(Counter());
expect(tester.terminalState, containsText('Count: 0'));
await tester.sendKey(LogicalKey.space);
expect(tester.terminalState, containsText('Count: 1'));
},
);
});
}
testNocterm function
testNocterm runs a test with a virtual terminal:
await testNocterm(
'test description',
(tester) async {
// Test code here
},
size: Size(80, 24), // Optional terminal size
debugPrintAfterPump: false, // Optional debug output
);
NoctermTester
The tester provides methods for testing:
// Render a component
await tester.pumpComponent(MyComponent());
// Send a key press
await tester.sendKey(LogicalKey.enter);
// Send multiple keys
await tester.sendKeys([
LogicalKey.keyH,
LogicalKey.keyE,
LogicalKey.keyL,
LogicalKey.keyL,
LogicalKey.keyO,
]);
// Type text
await tester.enterText('hello');
// Access terminal state
final state = tester.terminalState;
Basic assertions
Use matchers to verify output:
test('text display', () async {
await testNocterm(
'displays text',
(tester) async {
await tester.pumpComponent(Text('Hello'));
// Check if text is present
expect(tester.terminalState, containsText('Hello'));
},
);
});
hasStyledText
Check text with specific styling:
expect(
tester.terminalState,
hasStyledText('Error', color: Colors.red),
);
Testing interactions
Simulate user input:
test('button click', () async {
await testNocterm(
'handles click',
(tester) async {
var clicked = false;
await tester.pumpComponent(
Focusable(
focused: true,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.enter) {
clicked = true;
return true;
}
return false;
},
child: Text('Press Enter'),
),
);
await tester.sendKey(LogicalKey.enter);
expect(clicked, isTrue);
},
);
});
Simulating keyboard input
// Single key
await tester.sendKey(LogicalKey.arrowDown);
// Multiple keys
await tester.sendKeys([
LogicalKey.keyC,
LogicalKey.keyT,
LogicalKey.keyR,
LogicalKey.keyL,
]);
// Type text
await tester.enterText('hello world');
// With modifiers
await tester.sendKey(
LogicalKey.keyS,
control: true, // Ctrl+S
);
Testing state changes
Test components that change over time:
test('counter state', () async {
await testNocterm(
'counter increments',
(tester) async {
await tester.pumpComponent(Counter());
expect(tester.terminalState, containsText('Count: 0'));
await tester.sendKey(LogicalKey.space);
expect(tester.terminalState, containsText('Count: 1'));
await tester.sendKey(LogicalKey.space);
expect(tester.terminalState, containsText('Count: 2'));
},
);
});
Test one thing at a time
// Good - focused test
test('displays title', () async {
await testNocterm('title', (tester) async {
await tester.pumpComponent(App());
expect(tester.terminalState, containsText('My App'));
});
});
// Bad - testing too much
test('everything', () async {
// Testing multiple unrelated things
});
Use descriptive test names
// Good
test('increments counter when space pressed', () async { ... });
// Bad
test('counter', () async { ... });
Test edge cases
test('handles empty list', () async { ... });
test('handles very long text', () async { ... });
test('handles terminal resize', () async { ... });
Clean up resources
test('disposes controllers', () async {
await testNocterm('disposal', (tester) async {
final controller = TextEditingController();
await tester.pumpComponent(TextField(controller: controller));
// Component should dispose controller
controller.dispose(); // Should not throw
});
});
Next steps
- Visual Testing - Debug with visual output
- Test Matchers - All available matchers
- Components - Build testable components