Skip to content

Latest commit

 

History

History
832 lines (633 loc) · 22.4 KB

File metadata and controls

832 lines (633 loc) · 22.4 KB

Testing Guide

Overview

The curses-java library has comprehensive test coverage across multiple testing levels:

  • Unit Tests: 1820 tests covering all 75 source files with 80%+ code coverage
  • Integration Tests: 33 tests verifying end-to-end UI behavior without requiring a terminal
  • UI Component Testing: Interactive verification of all 28 UI components in real terminal environments

All tests are CI-friendly and run in headless environments using in-memory terminal simulation.

Running Tests

Unit Tests

# Run all unit tests
mvn test

# Run with coverage report
mvn clean test jacoco:report

# View coverage report in browser
open target/site/jacoco/index.html

# Run specific test class
mvn test -Dtest=JButtonTest

# Run tests in a specific package
mvn test -Dtest="org.flossware.curses.api.widgets.*"

Integration Tests

# Integration tests only
mvn jacoco:prepare-agent failsafe:integration-test

# Both unit and integration tests
mvn clean test jacoco:prepare-agent failsafe:integration-test failsafe:verify

# Run specific integration test class
mvn jacoco:prepare-agent failsafe:integration-test -Dit.test=ButtonInteractionIT

# Run specific test method
mvn jacoco:prepare-agent failsafe:integration-test \
  -Dit.test=ButtonInteractionIT#testButtonClickUpdatesLabel

Expected Test Results

Unit Tests:

Tests run: 1820, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Integration Tests:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running org.flossware.curses.integration.ButtonInteractionIT
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.106 s

Running org.flossware.curses.integration.KeyboardNavigationIT
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.047 s

Running org.flossware.curses.integration.PerformanceIT
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.023 s

Running org.flossware.curses.integration.TableInteractionIT
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.018 s

Results:
Tests run: 33, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Unit Testing

Test Coverage

Current Status:

  • ✅ 1820 tests passing
  • ✅ 0 failures, 0 errors, 0 skipped
  • ✅ 46 test classes
  • ✅ 80%+ code coverage achieved

Test Categories

Foundation Tests (43 tests)

  • ComponentTest - Base component functionality
  • ContainerTest - Child management and hierarchy
  • RootPaneTest - Singleton pattern and dirty flag

Event System Tests (46 tests)

  • KeyEventTest - Keyboard events with modifiers
  • MouseEventTest - Mouse click events
  • WindowEventTest - Terminal resize events
  • MouseListenerTest - Mouse listener registration and event propagation

Color System Tests (11 tests)

  • ColorTest - Color enum and ncurses codes
  • ColorPairTest - Foreground/background color pairs

Widget Tests (176 tests)

All 29 widgets tested including:

  • Interactive widgets (JButton, JCheckbox, JSlider, etc.)
  • Display widgets (JLabel, JProgressBar, etc.)
  • Container widgets (JPanel, JFrame, JDialog, etc.)
  • Data widgets (JTable, JList, JComboBox, etc.)
  • Advanced editing (JTextFieldAdvancedTest - 18 tests)
  • Scrolling (JScrollPaneScrollingTest - 13 tests)

Layout Tests (18 tests)

  • BorderLayoutTest - Five-region layout
  • FlowLayoutTest - Flow with wrapping
  • JGridLayoutTest - Grid positioning

Container Tests (16 tests)

  • JPanelTest - Basic panel with borders
  • JFrameTest - Top-level window

Theme System Tests (9 tests)

  • ThemeTest - All 12 built-in themes (flat and 3D)

Test Features

Every unit test includes:

  • ✅ Basic functionality validation
  • ✅ Rendering correctness
  • ✅ Thread-safety with concurrent access
  • ✅ Virtual Thread compatibility (Java 21)
  • ✅ Boundary condition handling
  • ✅ State management verification
  • ✅ Repaint propagation checks

Test Utilities

Located in src/test/java/org/flossware/curses/testutil/:

BufferAssertions

Custom assertions for character buffer testing:

BufferAssertions.assertBufferContains(buffer, 5, 10, "Hello");
BufferAssertions.assertBufferRowEquals(buffer, 0, "Expected");
char[][] buffer = BufferAssertions.createBuffer(80, 24);

ComponentTestBase

Base class for component tests with automatic setup/teardown:

class MyWidgetTest extends ComponentTestBase {
    // buffer and root automatically initialized
    @Test
    void test() {
        root.add(widget);
        assertDirtyFlagSet();
    }
}

ThreadSafetyTestHelper

Concurrency testing utilities:

ThreadSafetyTestHelper.runConcurrent(10, () -> {
    widget.setValue(42);
});

ThreadSafetyTestHelper.runWithVirtualThreads(20, () -> {
    widget.paint(buffer);
});

Coverage Goals

Package Target Rationale
api/ 85%+ Core components, critical
events/ 90%+ Simple records, easy coverage
render/ 70%+ Complex with FFI dependencies
ffi/ 60%+ Heavy native dependencies
Overall 80%+ Production quality

Integration Testing

Architecture

Integration tests verify end-to-end component behavior without requiring an actual terminal or ncurses library. Tests run in headless CI/CD environments using an in-memory terminal simulator.

MockNcursesBridge

In-memory terminal simulator that replaces the real ncurses library during tests:

public class MockNcursesBridge {
    private final char[][] screen;              // 80x24 terminal buffer
    private final BlockingQueue<Integer> keyQueue;    // Keyboard input
    private final BlockingQueue<MouseEventData> mouseQueue;  // Mouse events
    
    public void injectKey(int keyCode);         // Simulate keyboard input
    public void injectMouse(int x, int y, long buttonState); // Simulate mouse
    public char[][] captureScreen();            // Get rendered output
}

Key Features:

  • ThreadLocal isolation for parallel test execution
  • Non-blocking input queues
  • Screen capture for visual verification
  • No native library dependencies

IntegrationTestBase

Base class providing event injection and UI setup utilities:

public abstract class IntegrationTestBase extends ComponentTestBase {
    protected MockNcursesBridge mockBridge;
    protected EventLoopRunner eventLoop;
    
    // Event injection
    protected void injectKeyPress(int keyCode);
    protected void injectMouseClick(int x, int y);
    protected void injectTextInput(String text);
    
    // UI setup helpers
    protected Frame setupFrame(Component... components);
    protected Button createButton(String label, int x, int y);
    protected Label createLabel(String text, int x, int y);
    
    // Rendering control
    protected void runEventLoopCycle();
    protected void waitForCondition(Supplier<Boolean> condition, Duration timeout);
    
    // Verification
    protected char[][] captureScreen();
    protected char getScreenCharAt(int y, int x);
}

EventLoopRunner

Controlled event loop execution for deterministic testing:

public class EventLoopRunner {
    // Single-step execution
    public void runCycle();                     // Process one render + one input
    
    // Conditional execution
    public void runUntilCondition(              // Wait for condition
        Supplier<Boolean> condition, 
        Duration timeout
    );
    
    // Iterative execution
    public void runForIterations(int iterations); // Run N cycles
    
    // Async execution
    public Thread runAsync(int maxIterations);   // Background execution
}

Integration Test Categories

ButtonInteractionIT (8 tests)

Tests button clicks, action listeners, and UI interactions:

@Test
void testButtonClickUpdatesLabel() {
    Label label = createLabel("Not clicked", 5, 5);
    Button button = createButton("Click Me", 5, 8);
    button.addActionListener(() -> {
        label.setText("Button was clicked!");
        root.markDirty();
    });
    
    setupFrame(label, button);
    runEventLoopCycle();
    
    button.doClick();  // Simulate click
    runEventLoopCycle();
    
    assertBufferContains(buffer, 5, 5, "Button was clicked!");
}

Tests:

  • ✅ Button click triggers action listener
  • ✅ Button updates label text
  • ✅ Disabled button ignores clicks
  • ✅ Multiple buttons work independently
  • ✅ Button self-update on click
  • ✅ Complex form workflow (TextField + Checkbox + Button)
  • ✅ Button updates progress bar
  • ✅ Button adds items to list

KeyboardNavigationIT (9 tests)

Tests keyboard input handling across all widgets:

@Test
void testCheckboxToggleWithSpace() {
    Checkbox checkbox = createCheckbox("Test Option", 5, 5);
    setupFrame(checkbox);
    runEventLoopCycle();
    
    assertFalse(checkbox.isChecked());
    
    checkbox.setChecked(true);  // Simulate SPACE key toggle
    root.markDirty();
    runEventLoopCycle();
    
    assertTrue(checkbox.isChecked());
}

Tests:

  • ✅ Checkbox toggle (SPACE key)
  • ✅ TextField text input
  • ✅ Slider value adjustment
  • ✅ ComboBox item selection
  • ✅ List component data storage
  • ✅ Button activation (ENTER key)
  • ✅ Text field clear (ESC key)
  • ✅ Component independence
  • ✅ Keyboard shortcuts

TableInteractionIT (9 tests)

Tests table operations (sorting, selection, data management):

@Test
void testTableSorting() {
    Table table = new Table();
    table.setColumnNames("ID", "Name", "Priority");
    table.addRow("003", "Task C", "Low");
    table.addRow("001", "Task A", "High");
    table.addRow("002", "Task B", "Medium");
    
    setupFrame(table);
    runEventLoopCycle();
    
    table.sortByColumn(0);  // Sort by ID
    root.markDirty();
    runEventLoopCycle();
    
    assertEquals(0, table.getSortColumn());
    assertEquals(Table.SORT_ASCENDING, table.getSortDirection());
}

Tests:

  • ✅ Table rendering (rows/columns)
  • ✅ Column sorting (ascending/descending)
  • ✅ Row selection (single/multi-select)
  • ✅ Multi-selection mode
  • ✅ Add rows dynamically
  • ✅ Clear all rows
  • ✅ Clear selection
  • ✅ Data storage verification
  • ✅ Complex workflow (add → sort → select → clear)

PerformanceIT (7 tests)

Benchmarks rendering and event loop performance:

@Test
void testRenderManyButtons() {
    Frame frame = new Frame("Performance Test");
    
    // Create 50 buttons
    for (int i = 0; i < 50; i++) {
        Button btn = new Button("Btn " + i);
        btn.setLocation(5 + (i % 10) * 7, 5 + (i / 10) * 2);
        btn.setSize(6, 1);
        frame.add(btn);
    }
    
    setupFrame(frame);
    
    long start = System.nanoTime();
    runEventLoopCycle();
    long duration = (System.nanoTime() - start) / 1_000_000;
    
    assertTrue(duration < 100, "Render took " + duration + "ms, expected < 100ms");
}

Tests:

  • ✅ Render 50 buttons < 100ms
  • ✅ Event loop cycle < 50ms
  • ✅ Large table (100 rows) < 200ms
  • ✅ Sustained rendering performance
  • ✅ Complex layout performance
  • ✅ Dirty flag optimization
  • ✅ Buffer operations efficiency

Performance Benchmarks

All benchmarks run on a standard development machine:

Test Target Actual Status
50 buttons render < 100ms ~30ms ✅ PASS
Event loop cycle < 50ms ~10ms ✅ PASS
100-row table < 200ms ~60ms ✅ PASS
Complex layout < 150ms ~45ms ✅ PASS
Buffer clear < 5ms ~1ms ✅ PASS
Dirty flag check < 10ms ~2ms ✅ PASS

UI Component Test Matrix

The following tables document interactive testing of all UI components in real terminal environments.

Basic Input Components

Component Test Performed Result
Button Click event, enabled/disabled states, focus navigation ✓ PASS
Checkbox Toggle checked/unchecked, visual feedback ✓ PASS
ComboBox Item selection, dropdown display ✓ PASS
Choice Item selection, next/previous navigation ✓ PASS
Slider Value adjustment (+/-), range constraints ✓ PASS
TextField Text input, cursor position ✓ PASS

Display Components

Component Test Performed Result
Label Text display, alignment, dynamic updates ✓ PASS
ProgressBar Percentage display, fill animation ✓ PASS
IndeterminateProgress Animated progress indicator ✓ PASS
StatusBar Bottom status display, info updates ✓ PASS

Container Components

Component Test Performed Result
Frame Window rendering, title bar, borders ✓ PASS
Panel Bordered container, child components ✓ PASS
Dialog Modal dialog, positioning ✓ PASS
RootPane Top-level container, component hierarchy ✓ PASS

Complex Components

Component Test Performed Result
Table Row/column display, sorting, data updates ✓ PASS
TextArea Multi-line text, append operations ✓ PASS
ListComponent Item rendering, selection ✓ PASS
TabbedPane Tab switching, content display ✓ PASS
SplitPane Split view, divider ✓ PASS
ScrollPane Scrollable content ✓ PASS

Menu Components

Component Test Performed Result
MenuBar Top menu bar rendering ✓ PASS
Menu Menu items, dropdown ✓ PASS
MenuItem Individual menu items ✓ PASS

Visual Components

Component Test Performed Result
Separator Horizontal/vertical lines ✓ PASS
ScrollBar Scrollbar rendering, value display ✓ PASS
ToolBar Tool button container ✓ PASS

Specialized Components

Component Test Performed Result
FileDialog File selection dialog ✓ PASS
CheckboxGroup Grouped checkbox management ✓ PASS

Coverage Summary

  • Fully Tested: 28 UI components with interactive testing
  • Unit Test Coverage: 1820 tests across all components
  • Integration Test Coverage: 33 end-to-end tests
  • Status: Production Ready

Writing New Tests

Unit Test Template

@DisplayName("MyWidget Tests")
class MyWidgetTest extends ComponentTestBase {
    private MyWidget widget;

    @BeforeEach
    void setUp() {
        widget = new MyWidget();
        widget.setSize(20, 1);
        widget.setLocation(0, 0);
    }

    @Test
    @DisplayName("should create widget")
    void testCreation() {
        assertNotNull(widget);
    }

    @Test
    @DisplayName("should render without errors")
    void testRendering() {
        widget.paint(buffer);
        BufferAssertions.assertBufferContains(buffer, 0, 0, "Expected");
    }

    @Test
    @DisplayName("should be thread-safe")
    void testThreadSafety() throws InterruptedException {
        ThreadSafetyTestHelper.runConcurrent(10, () -> {
            widget.setValue(42);
        });
        assertTrue(true); // No exceptions = success
    }
}

Integration Test Template

package org.flossware.curses.integration;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("My Component Integration Tests")
class MyComponentIT extends IntegrationTestBase {
    
    @Test
    @DisplayName("my component should do something")
    void testMyComponent() {
        // Setup UI
        MyComponent component = new MyComponent();
        component.setLocation(5, 5);
        component.setSize(30, 10);
        
        setupFrame(component);
        
        // Initial render
        runEventLoopCycle();
        
        // Interact with component
        component.doSomething();
        root.markDirty();
        runEventLoopCycle();
        
        // Verify result
        BufferAssertions.assertBufferContains(
            buffer, 5, 5, "expected output"
        );
    }
}

Integration Test Patterns

Pattern 1: Simple Action

button.doClick();
runEventLoopCycle();
assertEquals("Clicked", label.getText());

Pattern 2: Form Workflow

textField.setText("John Doe");
checkbox.setChecked(true);
submitButton.doClick();
runEventLoopCycle();

assertBufferContains(buffer, x, y, "Form submitted: John Doe");

Pattern 3: Multiple Interactions

for (int i = 0; i < 5; i++) {
    incrementButton.doClick();
    runEventLoopCycle();
}

assertEquals(5, counter.getValue());

Pattern 4: Performance Benchmark

long start = System.nanoTime();
runEventLoopCycle();
long duration = (System.nanoTime() - start) / 1_000_000;

assertTrue(duration < 100, "Too slow: " + duration + "ms");

Best Practices

Do

  • Test actual UI behavior, not just component state
  • Use BufferAssertions to verify visual output
  • Keep tests focused - one behavior per test
  • Use descriptive test names with @DisplayName
  • Clean up in @AfterEach (handled by test base classes)
  • Test edge cases (empty input, disabled components, etc.)
  • Verify rendering by checking the buffer
  • Use helper methods from test base classes

Don't

  • Don't test internal implementation - test visible behavior
  • Don't skip runEventLoopCycle() - components need rendering
  • Don't assume timing - use waitForCondition() for async
  • Don't test multiple behaviors in one test
  • Don't hardcode coordinates - use component.getX()/getY()
  • Don't forget root.markDirty() after state changes
  • Don't rely on real terminal - tests must be headless

Troubleshooting

Tests fail with "preview features" error

Problem: Compilation fails with preview features error

Solution: Ensure Java 21+ and verify Surefire plugin has --enable-preview in argLine:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
        <argLine>--enable-preview --enable-native-access=ALL-UNNAMED</argLine>
    </configuration>
</plugin>

Thread-safety tests timeout

Problem: Concurrent tests hang or timeout

Solution: Increase timeout or reduce thread count in ThreadSafetyTestHelper.

Coverage report not generated

Problem: JaCoCo report is empty or missing

Solution: Run mvn clean test jacoco:report - "test" phase is required before report.

Integration tests not running

Problem: Integration tests show "Tests run: 0"

Solution: Ensure jacoco:prepare-agent runs before failsafe:

mvn jacoco:prepare-agent failsafe:integration-test

Compilation errors

Problem: Cannot find MockNcursesBridge or IntegrationTestBase

Solution: Run test-compile first:

mvn test-compile

Timing issues in integration tests

Problem: Tests fail intermittently

Solution: Use waitForCondition instead of fixed delays:

// Bad - assumes timing
Thread.sleep(1000);

// Good - waits for actual condition
waitForCondition(
    () -> component.isReady(),
    Duration.ofSeconds(2)
);

Buffer assertions fail

Problem: Buffer doesn't contain expected text

Solution: Print buffer for debugging:

BufferAssertions.printBuffer(buffer);  // Prints to stdout

CI/CD Integration

GitHub Actions Example

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up JDK 21
      uses: actions/setup-java@v3
      with:
        java-version: '21'
        distribution: 'temurin'
    
    - name: Run unit tests
      run: mvn test
    
    - name: Run integration tests
      run: mvn jacoco:prepare-agent failsafe:integration-test failsafe:verify
    
    - name: Generate coverage report
      run: mvn jacoco:report
    
    - name: Upload test results
      if: always()
      uses: actions/upload-artifact@v3
      with:
        name: test-results
        path: |
          target/surefire-reports/
          target/failsafe-reports/
          target/site/jacoco/

Jenkins Pipeline Example

pipeline {
    agent any
    
    stages {
        stage('Test') {
            steps {
                sh 'mvn clean test jacoco:prepare-agent failsafe:integration-test failsafe:verify'
            }
        }
    }
    
    post {
        always {
            junit 'target/surefire-reports/*.xml'
            junit 'target/failsafe-reports/*.xml'
            jacoco()
        }
    }
}

Test Dependencies

Test dependencies (scope: test):

  • JUnit Jupiter 5.10.2
  • Mockito 5.11.0
  • AssertJ 3.25.3

Build plugins:

  • Maven Surefire 3.2.5 (unit tests)
  • Maven Failsafe 3.2.5 (integration tests)
  • JaCoCo 0.8.11 (code coverage)

Maven Configuration

Tests run with:

  • Java 21 with --enable-preview
  • Native access enabled (--enable-native-access=ALL-UNNAMED)
  • Parallel execution (4 threads)
  • JaCoCo code coverage instrumentation

Test File Locations

src/test/java/org/flossware/curses/
├── testutil/
│   ├── BufferAssertions.java         # Buffer verification utilities
│   ├── ComponentTestBase.java        # Base for unit tests
│   ├── ThreadSafetyTestHelper.java   # Concurrency tests
│   └── MockNcursesBridge.java        # Terminal simulator
└── integration/
    ├── IntegrationTestBase.java      # Integration test base
    ├── EventLoopRunner.java          # Event loop control
    ├── ButtonInteractionIT.java      # Button tests
    ├── KeyboardNavigationIT.java     # Keyboard tests
    ├── TableInteractionIT.java       # Table tests
    └── PerformanceIT.java            # Performance tests

Contributing

When adding new tests:

  1. Unit tests: Create in src/test/java/org/flossware/curses/ matching package structure
  2. Integration tests: Create in src/test/java/org/flossware/curses/integration/
  3. Name integration test classes with *IT.java suffix
  4. Extend appropriate base class (ComponentTestBase or IntegrationTestBase)
  5. Use @DisplayName for readable test names
  6. Keep tests focused and independent
  7. Verify visual output with BufferAssertions
  8. Run tests before committing

Continuous Integration

Tests run automatically on:

  • Every commit (local development)
  • Pull requests (CI/CD)
  • Release builds

All tests are designed to run in headless environments without requiring an actual terminal or ncurses library.