Merge _configs/ into config/ for single configuration directory. Update all path references. Changes: - Move _configs/* to config/ - Update .gitignore for new patterns - No code references to _configs/ found Impact: -1 root directory (layout_conventions.md compliance)
812 lines
19 KiB
Markdown
812 lines
19 KiB
Markdown
# TUI Testing Guide
|
|
|
|
Comprehensive testing guide for RataTui-based TUI applications in the Tools ecosystem.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Testing Philosophy](#testing-philosophy)
|
|
2. [Unit Testing Patterns](#unit-testing-patterns)
|
|
3. [Integration Testing](#integration-testing)
|
|
4. [Test Organization](#test-organization)
|
|
5. [Mocking Strategies](#mocking-strategies)
|
|
6. [Coverage Requirements](#coverage-requirements)
|
|
7. [Common Testing Scenarios](#common-testing-scenarios)
|
|
8. [Performance Testing](#performance-testing)
|
|
|
|
## Testing Philosophy
|
|
|
|
### Core Principles
|
|
|
|
1. **Separation of Concerns**: Test state separately from rendering
|
|
- State logic (app.rs) can be tested without TUI framework
|
|
- Rendering (screens) is harder to test but can be verified through state
|
|
|
|
2. **Minimum 15+ Tests Per Module**: Comprehensive coverage
|
|
- Unit tests in module files
|
|
- Integration tests in tests/ directory
|
|
- Focus on critical paths and edge cases
|
|
|
|
3. **Deterministic Tests**: No flakiness allowed
|
|
- Use fixed values, not system time
|
|
- No random data or timing dependencies
|
|
- Tests must pass 100% of the time
|
|
|
|
4. **Fast Tests**: Keep test suite quick
|
|
- Avoid I/O when possible
|
|
- Use in-memory structures
|
|
- Run in parallel (--test-threads=8)
|
|
|
|
## Unit Testing Patterns
|
|
|
|
### Pattern 1: Testing Application State
|
|
|
|
**File**: `src/app.rs` (inline tests)
|
|
|
|
```rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// === Initialization Tests ===
|
|
|
|
#[test]
|
|
fn test_app_state_new() {
|
|
let state = AppState::new();
|
|
|
|
// Verify initial state
|
|
assert_eq!(state.current_screen, ScreenType::List);
|
|
assert!(state.items.is_empty());
|
|
assert_eq!(state.selected_index, 0);
|
|
assert!(state.dirty); // Should be dirty initially
|
|
assert!(!state.should_quit);
|
|
assert!(state.form_name.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_app_state_default() {
|
|
// Default should equal new()
|
|
assert_eq!(AppState::default(), AppState::new());
|
|
}
|
|
|
|
// === Item Management Tests ===
|
|
|
|
#[test]
|
|
fn test_add_single_item() {
|
|
let mut state = AppState::new();
|
|
assert!(state.items.is_empty());
|
|
|
|
state.add_item("Item 1".to_string());
|
|
|
|
assert_eq!(state.items.len(), 1);
|
|
assert!(state.dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_multiple_items() {
|
|
let mut state = AppState::new();
|
|
|
|
state.add_item("Item 1".to_string());
|
|
state.add_item("Item 2".to_string());
|
|
state.add_item("Item 3".to_string());
|
|
|
|
assert_eq!(state.items.len(), 3);
|
|
assert_eq!(state.items[0], "Item 1");
|
|
assert_eq!(state.items[2], "Item 3");
|
|
}
|
|
|
|
#[test]
|
|
fn test_selected_item_empty() {
|
|
let state = AppState::new();
|
|
assert!(state.selected_item().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_selected_item_with_selection() {
|
|
let mut state = AppState::new();
|
|
state.add_item("Item 1".to_string());
|
|
|
|
assert_eq!(state.selected_item(), Some(&"Item 1".to_string()));
|
|
}
|
|
|
|
// === Navigation Tests ===
|
|
|
|
#[test]
|
|
fn test_select_next_single_item() {
|
|
let mut state = AppState::new();
|
|
state.add_item("Item 1".to_string());
|
|
state.reset_dirty();
|
|
|
|
state.select_next();
|
|
|
|
// Wraps around with single item
|
|
assert_eq!(state.selected_index, 0);
|
|
assert!(state.dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_next_multiple_items() {
|
|
let mut state = AppState::new();
|
|
state.add_item("Item 1".to_string());
|
|
state.add_item("Item 2".to_string());
|
|
state.add_item("Item 3".to_string());
|
|
state.reset_dirty();
|
|
|
|
state.select_next();
|
|
assert_eq!(state.selected_index, 1);
|
|
assert!(state.dirty);
|
|
|
|
state.select_next();
|
|
assert_eq!(state.selected_index, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_next_wraps_around() {
|
|
let mut state = AppState::new();
|
|
state.add_item("Item 1".to_string());
|
|
state.add_item("Item 2".to_string());
|
|
state.selected_index = 1; // Last item
|
|
state.reset_dirty();
|
|
|
|
state.select_next();
|
|
|
|
assert_eq!(state.selected_index, 0); // Wraps to first
|
|
assert!(state.dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_previous() {
|
|
let mut state = AppState::new();
|
|
state.add_item("Item 1".to_string());
|
|
state.add_item("Item 2".to_string());
|
|
state.selected_index = 1;
|
|
state.reset_dirty();
|
|
|
|
state.select_previous();
|
|
|
|
assert_eq!(state.selected_index, 0);
|
|
assert!(state.dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_previous_from_first_wraps() {
|
|
let mut state = AppState::new();
|
|
state.add_item("Item 1".to_string());
|
|
state.add_item("Item 2".to_string());
|
|
state.selected_index = 0;
|
|
state.reset_dirty();
|
|
|
|
state.select_previous();
|
|
|
|
assert_eq!(state.selected_index, 1); // Wraps to last
|
|
assert!(state.dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_on_empty_list_no_panic() {
|
|
let mut state = AppState::new();
|
|
// No panic should occur on empty list
|
|
state.select_next();
|
|
state.select_previous();
|
|
assert_eq!(state.selected_index, 0);
|
|
}
|
|
|
|
// === Screen Navigation Tests ===
|
|
|
|
#[test]
|
|
fn test_set_screen_changes_screen() {
|
|
let mut state = AppState::new();
|
|
assert_eq!(state.current_screen, ScreenType::List);
|
|
state.reset_dirty();
|
|
|
|
state.set_screen(ScreenType::Create);
|
|
|
|
assert_eq!(state.current_screen, ScreenType::Create);
|
|
assert!(state.dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn test_set_screen_same_screen_not_dirty() {
|
|
let mut state = AppState::new();
|
|
state.set_screen(ScreenType::List);
|
|
state.reset_dirty();
|
|
|
|
// Setting to same screen
|
|
state.set_screen(ScreenType::List);
|
|
|
|
// Should NOT mark dirty (optimization)
|
|
assert!(!state.dirty);
|
|
}
|
|
|
|
// === Form Management Tests ===
|
|
|
|
#[test]
|
|
fn test_reset_form_clears_fields() {
|
|
let mut state = AppState::new();
|
|
state.form_name = "Test Name".to_string();
|
|
state.form_description = "Test Description".to_string();
|
|
state.reset_dirty();
|
|
|
|
state.reset_form();
|
|
|
|
assert!(state.form_name.is_empty());
|
|
assert!(state.form_description.is_empty());
|
|
assert!(state.dirty);
|
|
}
|
|
|
|
// === Dirty Flag Tests ===
|
|
|
|
#[test]
|
|
fn test_dirty_flag_initially_true() {
|
|
let state = AppState::new();
|
|
assert!(state.is_dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_dirty_flag_reset() {
|
|
let mut state = AppState::new();
|
|
assert!(state.is_dirty());
|
|
|
|
state.reset_dirty();
|
|
|
|
assert!(!state.is_dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_dirty_flag_after_operations() {
|
|
let mut state = AppState::new();
|
|
state.reset_dirty();
|
|
assert!(!state.is_dirty());
|
|
|
|
// Any mutation should mark dirty
|
|
state.add_item("Item".to_string());
|
|
assert!(state.is_dirty());
|
|
|
|
state.reset_dirty();
|
|
state.select_next();
|
|
assert!(state.is_dirty());
|
|
}
|
|
|
|
// === Lifecycle Tests ===
|
|
|
|
#[test]
|
|
fn test_request_quit() {
|
|
let mut state = AppState::new();
|
|
assert!(!state.should_quit());
|
|
|
|
state.request_quit();
|
|
|
|
assert!(state.should_quit());
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern 2: Testing Screen Rendering
|
|
|
|
**File**: `src/screens/list.rs` (inline tests)
|
|
|
|
**Note**: Rendering tests verify state interpretation, not visual output.
|
|
|
|
```rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_list_screen_empty_state() {
|
|
let state = AppState::new();
|
|
|
|
// Verify state for empty rendering
|
|
assert!(state.items.is_empty());
|
|
// Empty state should be handled gracefully
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_screen_with_items() {
|
|
let mut state = AppState::new();
|
|
state.add_item("Item 1".to_string());
|
|
state.add_item("Item 2".to_string());
|
|
|
|
assert_eq!(state.items.len(), 2);
|
|
assert_eq!(state.selected_index, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_screen_selected_index_valid() {
|
|
let mut state = AppState::new();
|
|
state.add_item("Item 1".to_string());
|
|
state.add_item("Item 2".to_string());
|
|
|
|
// Verify selected item exists
|
|
assert!(state.selected_item().is_some());
|
|
assert_eq!(state.selected_item(), Some(&"Item 1".to_string()));
|
|
|
|
state.select_next();
|
|
assert_eq!(state.selected_item(), Some(&"Item 2".to_string()));
|
|
}
|
|
}
|
|
```
|
|
|
|
## Integration Testing
|
|
|
|
### Pattern 3: Full Workflow Tests
|
|
|
|
**File**: `tests/integration_tests.rs`
|
|
|
|
```rust
|
|
//! Integration tests - test complete workflows
|
|
|
|
use app_name::{AppState, ScreenType};
|
|
|
|
#[test]
|
|
fn test_complete_list_workflow() {
|
|
// Create state
|
|
let mut state = AppState::new();
|
|
assert_eq!(state.current_screen, ScreenType::List);
|
|
|
|
// Add items
|
|
state.add_item("Item 1".to_string());
|
|
state.add_item("Item 2".to_string());
|
|
state.add_item("Item 3".to_string());
|
|
assert_eq!(state.items.len(), 3);
|
|
|
|
// Navigate through items
|
|
state.reset_dirty();
|
|
state.select_next();
|
|
assert_eq!(state.selected_index, 1);
|
|
assert!(state.is_dirty());
|
|
|
|
state.select_next();
|
|
assert_eq!(state.selected_index, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_complete_create_workflow() {
|
|
let mut state = AppState::new();
|
|
|
|
// Navigate to create screen
|
|
state.set_screen(ScreenType::Create);
|
|
assert_eq!(state.current_screen, ScreenType::Create);
|
|
|
|
// Fill form
|
|
state.form_name = "New Item".to_string();
|
|
state.form_description = "Description".to_string();
|
|
|
|
// Submit (in real app, this happens in main loop)
|
|
state.add_item(state.form_name.clone());
|
|
|
|
// Return to list
|
|
state.reset_form();
|
|
state.set_screen(ScreenType::List);
|
|
|
|
// Verify item was added
|
|
assert_eq!(state.items.len(), 1);
|
|
assert_eq!(state.items[0], "New Item");
|
|
assert!(state.form_name.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_navigation_between_screens() {
|
|
let mut state = AppState::new();
|
|
|
|
// Start at list
|
|
assert_eq!(state.current_screen, ScreenType::List);
|
|
|
|
// Navigate to detail
|
|
state.set_screen(ScreenType::Detail);
|
|
assert_eq!(state.current_screen, ScreenType::Detail);
|
|
|
|
// Navigate to create
|
|
state.set_screen(ScreenType::Create);
|
|
assert_eq!(state.current_screen, ScreenType::Create);
|
|
|
|
// Back to list
|
|
state.set_screen(ScreenType::List);
|
|
assert_eq!(state.current_screen, ScreenType::List);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dirty_flag_optimization() {
|
|
let mut state = AppState::new();
|
|
|
|
// Initial state should be dirty
|
|
assert!(state.is_dirty());
|
|
|
|
// Reset after render
|
|
state.reset_dirty();
|
|
assert!(!state.is_dirty());
|
|
|
|
// No changes - should not be dirty
|
|
let _ = state.selected_item();
|
|
assert!(!state.is_dirty());
|
|
|
|
// Add item - should be dirty
|
|
state.add_item("Item 1".to_string());
|
|
assert!(state.is_dirty());
|
|
|
|
// Reset
|
|
state.reset_dirty();
|
|
assert!(!state.is_dirty());
|
|
|
|
// Navigate - should be dirty
|
|
state.select_next();
|
|
assert!(state.is_dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_selection_with_item_operations() {
|
|
let mut state = AppState::new();
|
|
|
|
// Add items
|
|
state.add_item("Item 1".to_string());
|
|
state.add_item("Item 2".to_string());
|
|
state.add_item("Item 3".to_string());
|
|
|
|
// Select specific item
|
|
state.selected_index = 1;
|
|
assert_eq!(state.selected_item(), Some(&"Item 2".to_string()));
|
|
|
|
// Navigate and verify
|
|
state.select_next();
|
|
assert_eq!(state.selected_item(), Some(&"Item 3".to_string()));
|
|
|
|
// Navigate with wrap
|
|
state.select_next();
|
|
assert_eq!(state.selected_item(), Some(&"Item 1".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_form_reset_clears_all_fields() {
|
|
let mut state = AppState::new();
|
|
|
|
// Populate form
|
|
state.form_name = "Name".to_string();
|
|
state.form_description = "Description".to_string();
|
|
|
|
// Reset
|
|
state.reset_form();
|
|
|
|
// All fields empty
|
|
assert!(state.form_name.is_empty());
|
|
assert!(state.form_description.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_quit_flag_persistence() {
|
|
let mut state = AppState::new();
|
|
assert!(!state.should_quit());
|
|
|
|
state.request_quit();
|
|
assert!(state.should_quit());
|
|
|
|
// Flag persists until explicitly cleared
|
|
// (in real app, app loop checks and exits)
|
|
}
|
|
```
|
|
|
|
## Test Organization
|
|
|
|
### Directory Structure
|
|
|
|
```
|
|
crate-root/
|
|
├── src/
|
|
│ ├── lib.rs # Public API
|
|
│ ├── app.rs # +15 tests (inline)
|
|
│ ├── screens/
|
|
│ │ ├── mod.rs
|
|
│ │ ├── list.rs # +2 tests (inline)
|
|
│ │ ├── create.rs # +2 tests (inline)
|
|
│ │ └── detail.rs # +2 tests (inline)
|
|
│ └── integration.rs # +5 tests (inline)
|
|
│
|
|
└── tests/ # Integration tests
|
|
├── integration_tests.rs # +10+ tests
|
|
└── workflows.rs # +5+ tests
|
|
```
|
|
|
|
### Running Tests
|
|
|
|
```bash
|
|
# All tests
|
|
cargo test --lib
|
|
|
|
# Specific module
|
|
cargo test --lib app::
|
|
|
|
# Single test
|
|
cargo test --lib app::test_app_state_new
|
|
|
|
# With output
|
|
cargo test --lib -- --nocapture
|
|
|
|
# Parallel execution (faster)
|
|
cargo test --lib -- --test-threads=8
|
|
|
|
# Single-threaded (for debugging)
|
|
cargo test --lib -- --test-threads=1
|
|
|
|
# Integration tests only
|
|
cargo test --test '*'
|
|
|
|
# Coverage report (requires cargo-tarpaulin)
|
|
cargo tarpaulin --lib
|
|
```
|
|
|
|
## Mocking Strategies
|
|
|
|
### Pattern 4: Mocking Complex Dependencies
|
|
|
|
If your AppState depends on external services:
|
|
|
|
```rust
|
|
// src/app.rs
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// Mock service
|
|
struct MockItemService {
|
|
items: Vec<String>,
|
|
}
|
|
|
|
impl MockItemService {
|
|
fn new() -> Self {
|
|
MockItemService {
|
|
items: vec![
|
|
"Item 1".to_string(),
|
|
"Item 2".to_string(),
|
|
],
|
|
}
|
|
}
|
|
|
|
fn fetch_items(&self) -> Vec<String> {
|
|
self.items.clone()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_with_mocked_service() {
|
|
let service = MockItemService::new();
|
|
let items = service.fetch_items();
|
|
|
|
assert_eq!(items.len(), 2);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern 5: Testing State Transitions
|
|
|
|
```rust
|
|
#[test]
|
|
fn test_state_transition_sequence() {
|
|
let mut state = AppState::new();
|
|
let mut actions = vec![];
|
|
|
|
// Record actions
|
|
actions.push(("initial", state.current_screen));
|
|
|
|
state.add_item("Item 1".to_string());
|
|
actions.push(("add_item", state.current_screen));
|
|
|
|
state.set_screen(ScreenType::Create);
|
|
actions.push(("set_screen", state.current_screen));
|
|
|
|
// Verify sequence
|
|
assert_eq!(actions[0].0, "initial");
|
|
assert_eq!(actions[1].0, "add_item");
|
|
assert_eq!(actions[2].0, "set_screen");
|
|
assert_eq!(actions[2].1, ScreenType::Create);
|
|
}
|
|
```
|
|
|
|
## Coverage Requirements
|
|
|
|
### Minimum Test Coverage
|
|
|
|
**Per Module Requirement**: 15+ tests
|
|
|
|
| Module | Tests | Focus |
|
|
|--------|-------|-------|
|
|
| app.rs | 18+ | State mutations, navigation, lifecycle |
|
|
| list.rs | 4+ | Empty state, item display, selection |
|
|
| create.rs | 3+ | Form handling, field management |
|
|
| detail.rs | 3+ | Selection handling, empty state |
|
|
| integration.rs | 5+ | Dashboard integration, command execution |
|
|
| **Total** | **35+** | All major paths |
|
|
|
|
### Coverage Verification
|
|
|
|
```bash
|
|
# Install tarpaulin
|
|
cargo install cargo-tarpaulin
|
|
|
|
# Run coverage
|
|
cargo tarpaulin --lib --out Html
|
|
|
|
# View report
|
|
open tarpaulin-report.html
|
|
```
|
|
|
|
### Coverage Targets
|
|
|
|
- **Lines**: 80%+ coverage
|
|
- **Branches**: 75%+ coverage
|
|
- **Functions**: 90%+ coverage
|
|
- **Zero** untested critical paths
|
|
|
|
## Common Testing Scenarios
|
|
|
|
### Scenario 1: Empty State Handling
|
|
|
|
```rust
|
|
#[test]
|
|
fn test_empty_list_no_panic() {
|
|
let mut state = AppState::new();
|
|
|
|
// These should not panic
|
|
state.select_next();
|
|
state.select_previous();
|
|
let _ = state.selected_item();
|
|
|
|
assert_eq!(state.selected_index, 0);
|
|
assert!(state.items.is_empty());
|
|
}
|
|
```
|
|
|
|
### Scenario 2: Boundary Conditions
|
|
|
|
```rust
|
|
#[test]
|
|
fn test_selection_boundaries() {
|
|
let mut state = AppState::new();
|
|
state.add_item("Only Item".to_string());
|
|
|
|
// Single item wraps to itself
|
|
state.select_next();
|
|
assert_eq!(state.selected_index, 0);
|
|
|
|
state.select_previous();
|
|
assert_eq!(state.selected_index, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_large_item_count() {
|
|
let mut state = AppState::new();
|
|
|
|
// Add 1000 items
|
|
for i in 0..1000 {
|
|
state.add_item(format!("Item {}", i));
|
|
}
|
|
|
|
assert_eq!(state.items.len(), 1000);
|
|
|
|
// Navigation still works
|
|
for _ in 0..1000 {
|
|
state.select_next();
|
|
}
|
|
assert_eq!(state.selected_index, 0); // Wraps around
|
|
}
|
|
```
|
|
|
|
### Scenario 3: State Invariants
|
|
|
|
```rust
|
|
#[test]
|
|
fn test_selected_index_invariant() {
|
|
let mut state = AppState::new();
|
|
|
|
// Add and remove items - selected_index should remain valid
|
|
state.add_item("Item 1".to_string());
|
|
assert!(state.selected_index < state.items.len());
|
|
|
|
state.add_item("Item 2".to_string());
|
|
assert!(state.selected_index < state.items.len());
|
|
|
|
state.select_next();
|
|
assert!(state.selected_index < state.items.len());
|
|
}
|
|
```
|
|
|
|
### Scenario 4: Dirty Flag Correctness
|
|
|
|
```rust
|
|
#[test]
|
|
fn test_dirty_flag_comprehensive() {
|
|
let mut state = AppState::new();
|
|
let mut changes = vec![];
|
|
|
|
state.reset_dirty();
|
|
changes.push(("after_reset", state.is_dirty()));
|
|
|
|
state.add_item("Item".to_string());
|
|
changes.push(("after_add", state.is_dirty()));
|
|
|
|
state.reset_dirty();
|
|
changes.push(("after_reset_2", state.is_dirty()));
|
|
|
|
state.set_screen(ScreenType::Create);
|
|
changes.push(("after_set_screen", state.is_dirty()));
|
|
|
|
// Verify pattern
|
|
assert!(!changes[0].1); // Should be clean
|
|
assert!(changes[1].1); // Should be dirty
|
|
assert!(!changes[2].1); // Should be clean
|
|
assert!(changes[3].1); // Should be dirty
|
|
}
|
|
```
|
|
|
|
## Performance Testing
|
|
|
|
### Benchmark Template
|
|
|
|
If performance is critical, add benchmarks:
|
|
|
|
```toml
|
|
# Cargo.toml
|
|
[[bench]]
|
|
name = "app_state_operations"
|
|
harness = false
|
|
```
|
|
|
|
Create `benches/app_state_operations.rs`:
|
|
|
|
```rust
|
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
use your_tui::{AppState};
|
|
|
|
fn bench_add_items(c: &mut Criterion) {
|
|
c.bench_function("add_1000_items", |b| {
|
|
b.iter(|| {
|
|
let mut state = AppState::new();
|
|
for i in 0..1000 {
|
|
state.add_item(black_box(format!("Item {}", i)));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn bench_select_next(c: &mut Criterion) {
|
|
let mut state = AppState::new();
|
|
for i in 0..1000 {
|
|
state.add_item(format!("Item {}", i));
|
|
}
|
|
|
|
c.bench_function("select_next_1000_items", |b| {
|
|
b.iter(|| {
|
|
state.select_next();
|
|
});
|
|
});
|
|
}
|
|
|
|
criterion_group!(benches, bench_add_items, bench_select_next);
|
|
criterion_main!(benches);
|
|
```
|
|
|
|
Run benchmarks:
|
|
```bash
|
|
cargo bench
|
|
```
|
|
|
|
## Summary
|
|
|
|
**Key Testing Rules**:
|
|
1. Minimum 15+ tests per module
|
|
2. Test state separately from rendering
|
|
3. Use integration tests for full workflows
|
|
4. Verify dirty flag behavior (critical for performance)
|
|
5. Test edge cases and boundaries
|
|
6. Keep tests deterministic and fast
|
|
7. Achieve 80%+ code coverage
|
|
8. All tests must pass before committing
|
|
|
|
**Testing Checklist**:
|
|
- [ ] 15+ tests in app.rs
|
|
- [ ] 2-3 tests per screen
|
|
- [ ] 5+ tests in integration module
|
|
- [ ] 10+ integration tests in tests/
|
|
- [ ] All edge cases covered
|
|
- [ ] All tests pass: `cargo test --lib`
|
|
- [ ] Coverage > 80%: `cargo tarpaulin --lib`
|
|
- [ ] No panics or unwrap() in tested code
|