---
name: pytest-testing
description: 'Write high-quality BDD pytest tests for the ArduPilot Methodic Configurator. Use when writing new unit tests, creating test fixtures, applying Given-When-Then pattern, testing tkinter frontends, or following the minimal-mocking strategy for backend and business logic.'
---

# Pytest Testing Guidelines

> **Note**: This is a comprehensive developer documentation file. For GitHub Copilot repository instructions, see [`.github/copilot-instructions.md`](../../copilot-instructions.md).

## For AI Agents and Senior Developers

This document provides comprehensive guidelines for writing high-quality,
behavior-driven pytest tests for the ArduPilot Methodic Configurator project.
These guidelines ensure consistency, maintainability, and comprehensive test
coverage while following industry best practices.

## Core Testing Philosophy

### 1. Senior Developer Mindset

- Write tests that **future developers will thank you for**
- Focus on **behavior over implementation details**
- Prioritize **maintainability and readability**
- Use **minimal, strategic mocking**
- Apply **DRY principles consistently**

### 2. Behavior-Driven Development (BDD)

- Write tests that describe **user behavior** and **business value**,
  not implementation details
- Use Given-When-Then structure in test descriptions and code comments
- Focus on **what** the system should do, not **how** it does it
- Test names should read like specifications:
  `test_user_can_select_template_by_double_clicking`

### 3. Test Isolation and Independence

- Each test should be completely independent and able to run in any order
- Use fixtures with appropriate scopes (`function`, `class`, `module`, `session`)
- Clean up resources properly after tests complete
- Avoid shared mutable state between tests

All tests MUST follow this structure:

```python
def test_descriptive_behavior_name(self, fixture_name) -> None:
    """
    Brief summary of what the test validates.

    GIVEN: Initial system state and preconditions
    WHEN: The action or event being tested
    THEN: Expected outcomes and assertions
    """
    # Arrange (Given): Set up test data and mocks

    # Act (When): Execute the behavior being tested

    # Assert (Then): Verify expected outcomes
```

## 🔧 Fixture Guidelines

### Required Fixture Pattern

Create reusable fixtures that eliminate code duplication:

```python
@pytest.fixture
def mock_vehicle_provider() -> MagicMock:
    """Fixture providing a mock vehicle components provider with realistic test data."""
    provider = MagicMock()

    # Create properly structured mock data matching real object interfaces
    template_1 = MagicMock()
    template_1.attributes.return_value = ["name", "fc", "gnss"]
    template_1.name = "QuadCopter X"
    template_1.fc = "Pixhawk 6C"
    template_1.gnss = "Here3+"

    provider.get_vehicle_components_overviews.return_value = {
        "Copter/QuadX": template_1,
    }
    return provider


@pytest.fixture
def configured_window(mock_vehicle_provider) -> ComponentWindow:
    """Fixture providing a properly configured window for behavior testing."""
    with (
        patch("tkinter.Toplevel"),
        patch.object(BaseWindow, "__init__", return_value=None),
        # Only patch what's necessary for the test
    ):
        window = ComponentWindow(vehicle_components_provider=mock_vehicle_provider)
        window.root = MagicMock()
        return window
```

### Fixture Design Rules

1. **One concern per fixture** - Each fixture should have a single responsibility
2. **Realistic mock data** - Use data that mirrors real system behavior
3. **Composable fixtures** - Allow fixtures to depend on other fixtures
4. **Descriptive names** - Fixture names should clearly indicate their purpose
5. **Minimal scope** - Use the narrowest scope possible (function > class > module > session)

## 📝 Test Structure Standards

### Test Class Organization

```python
class TestUserWorkflow:
    """Test complete user workflows and interactions."""

    def test_user_can_complete_primary_task(self, configured_window) -> None:
        """
        User can successfully complete the primary workflow.

        GIVEN: A user opens the application
        WHEN: They follow the standard workflow
        THEN: They should achieve their goal without errors
        """
        # Implementation
```

### Test Method Requirements

1. **Descriptive names** - `test_user_can_select_template_by_double_clicking`
2. **Single behavior focus** - Test one behavior per method
3. **User-centric language** - Write from the user's perspective
4. **Complete documentation** - Include summary and GIVEN/WHEN/THEN

### Assertions

- Use **specific assertions** over generic ones
- **Test behavior outcomes** not implementation details
- **Group related assertions** logically
- **Include meaningful failure messages** when helpful

```python
# Good - Tests behavior
assert window.selected_template == "Copter/QuadX"
assert window.close_window.called_once()

# Bad - Tests implementation
assert mock_method.call_count == 1
```

## 🎭 Mocking Strategy

### Minimal Mocking Principle

Only mock what is **absolutely necessary**:

```python
# Good - Minimal mocking
def test_template_selection(self, template_window) -> None:
    template_window.tree.selection.return_value = ["item1"]
    template_window._on_selection_change(mock_event)
    assert template_window.selected_template == "expected_value"

# Bad - Over-mocking
def test_template_selection(self) -> None:
    with patch("tkinter.Tk"), \
         patch("module.Class1"), \
         patch("module.Class2"), \
         patch("module.method1"), \
         patch("module.method2"):
        # Test lost in mocking complexity
```

### Mocking Guidelines

1. **Mock external dependencies** (file system, network, databases)
2. **Mock UI framework calls** (tkinter widgets, events)
3. **Don't mock the system under test** - Test real behavior
4. **Use fixtures for complex mocks** - Keep test methods clean
5. **Mock return values realistically** - Match expected data types and structures

## 🏗️ Project-Specific Patterns

### Frontend Tkinter Testing

```python
# Use template_overview_window_setup fixture for complex UI mocking
def test_ui_behavior(self, template_overview_window_setup) -> None:
    """Test UI behavior without full window creation."""


# Use template_window fixture for component testing
def test_component_behavior(self, template_window) -> None:
    """Test component behavior with configured window."""
```

### Backend/Logic Testing

```python
# Use minimal mocking for business logic
def test_business_logic(self, mock_data_provider) -> None:
    """Test core logic with realistic data."""


# Mock only external dependencies
@patch("module.external_api_call")
def test_integration_behavior(self, mock_api) -> None:
    """Test integration points."""
```

### macOS CI Headless Testing (Tkinter Segfault Guidelines)

Tkinter is extremely buggy and unstable on macOS GitHub Action runners.
Without a physical display, forcing UI updates will cause Segmentation
Faults (`AppKit`/`HIToolbox` crashes) which instantly kill the MacOS Pytest runner.
To prevent this, please follow these rules:

1. **Never use `.update()`**: It forces a full event loop evaluation and will crash macOS.
Always use `.update_idletasks()` instead.
2. Preventing CI Crashes During Setup: Whenever you create a Tkinter window in a test,
you must block the main tkinter library from trying to render to a physical screen.
Make sure your yield statement stays inside the with patch(...) block so that
these overrides don't expire before the test finishes running.

```python
@pytest.fixture
def safe_window_fixture(tk_root) -> Generator[MyWindow, None, None]:
    with (
        # Prevent Tkinter DPI scaling calculations from crashing macOS
        patch.object(tk.Toplevel, "winfo_fpixels", side_effect=tk.TclError("no display")),
        # Block macOS C-level screen rendering universally
        patch("tkinter.Misc.update"),
        patch("tkinter.Misc.update_idletasks"),
        patch("tkinter.Misc.wait_visibility"),
        patch("tkinter.Misc.wait_window"),
    ):
        window = MyWindow(tk_root)

        # Follow Point 2
        yield window

- GUI Tests (gui_*.py): Any test simulating physical interaction (e.g., PyAutoGUI) must
explicitly load the CI environment setup at the very top of the file:

from tests.conftest import gui_test_environment  # noqa: F401 # pylint: disable=unused-import
pytestmark = pytest.mark.gui

## 📋 Test Categories

### Required Test Types

1. **User Workflow Tests** - Complete user journeys
2. **Component Behavior Tests** - Individual component functionality
3. **Error Handling Tests** - Graceful failure scenarios
4. **Integration Tests** - Component interaction validation
5. **Edge Case Tests** - Boundary conditions and unusual inputs

### Test Organization

Test files follow specific naming conventions to clearly indicate their purpose and scope:

```text
tests/
├── test_frontend_tkinter_component.py      # BDD UI component unit tests
├── test_backend_logic.py                   # BDD Business logic unit tests
├── gui_*.py                                # pyautogui GUI-focused tests (prefixed with gui_)
├── integration_*.py                        # End-to-end integration tests (prefixed with integration_)
├── acceptance_*.py                         # Acceptance tests (prefixed with acceptance_)
├── unit_*.py                               # Low-level unit tests for coverage purposes (prefixed with unit_)
└── conftest.py                             # Shared fixtures
```

## 📊 Quality Standards

### Test Quality Metrics

- **Coverage**: Aim for 80%+ on core modules
- **Maintainability**: Tests should be easy to modify
- **Speed**: Test suite should run in < 2 minutes
- **Reliability**: Zero flaky tests allowed

### Code Review Checklist

- [ ] Tests follow GIVEN/WHEN/THEN structure
- [ ] Fixtures used instead of repeated setup
- [ ] Minimal, strategic mocking applied
- [ ] User-focused test names and descriptions
- [ ] All edge cases covered
- [ ] Error scenarios tested
- [ ] Performance considerations addressed

## 🚀 Example: Complete Test Implementation

```python
class TestTemplateSelection:
    """Test user template selection workflows."""

    def test_user_can_select_template_by_double_clicking(self, template_window) -> None:
        """
        User can select a vehicle template by double-clicking on the tree item.

        GIVEN: A user views available vehicle templates
        WHEN: They double-click on a specific template row
        THEN: The template should be selected and stored
        AND: The window should close automatically
        """
        # Arrange: Configure template selection behavior
        template_window.tree.identify_row.return_value = "template_item"
        template_window.tree.item.return_value = {"text": "Copter/QuadX"}

        # Act: User double-clicks on template
        mock_event = MagicMock(y=100)
        template_window._on_row_double_click(mock_event)

        # Assert: Template selected and workflow completed
        template_window.program_settings_provider.store_template_dir.assert_called_once_with("Copter/QuadX")
        template_window.root.destroy.assert_called_once()

    def test_user_sees_visual_feedback_during_selection(self, template_window) -> None:
        """
        User receives immediate visual feedback when selecting templates.

        GIVEN: A user is browsing available templates
        WHEN: They click on a template row
        THEN: The corresponding vehicle image should be displayed immediately
        """
        # Arrange: Set up selection behavior
        template_window.tree.selection.return_value = ["selected_item"]
        template_window.tree.item.return_value = {"text": "Plane/FixedWing"}

        with patch.object(template_window, "_display_vehicle_image") as mock_display:
            # Act: User selects template
            mock_event = MagicMock()
            template_window._on_row_selection_change(mock_event)
            template_window._update_selection()  # Simulate callback

            # Assert: Visual feedback provided
            mock_display.assert_called_once_with("Plane/FixedWing")
```

## 🛠️ Development Workflow

### Pre-commit Requirements

1. **Run tests**: `pytest tests/ -v`
2. **Check coverage**: `pytest --cov=ardupilot_methodic_configurator --cov-report=term-missing`
3. **Format with ruff**: `ruff format`
4. **Lint with ruff**: `ruff check --fix`
5. **Type check with mypy**: `mypy`
6. **Advanced type check with pyright**: `pyright`
7. **Style check with pylint**: `pylint $(git ls-files '*.py')`

### Test Execution Commands

On Linux systems where the normal display is unavailable, run pytest through
the project virtual environment and Xvfb. This is required by GUI-dependent
fixtures such as PyAutoGUI:

```bash
# Run non-SITL tests with the local virtual environment and a virtual display
PATH="$PWD/.venv/bin:$PATH" xvfb-run -a python -m pytest tests/ -v -m "not sitl and not integration"
```

```bash
# Run all tests with verbose output
pytest tests/ -v

# Run tests with coverage reporting
pytest tests/ --cov=ardupilot_methodic_configurator --cov-report=html

# Run specific test file
pytest tests/test_frontend_tkinter_template_overview.py -v

# Run tests matching pattern
pytest tests/ -k "test_user" -v

# Run tests with performance timing
pytest tests/ --durations=10
```

### Debugging Failed Tests

```bash
# Run with detailed output
pytest tests/test_file.py::test_method -v -s

# Run with pdb debugging
pytest tests/test_file.py::test_method --pdb

# Run with custom markers
pytest tests/ -m "slow" -v
```

## 🔍 Quality Assurance

### Success Criteria

- ✅ All tests pass consistently
- ✅ Coverage ≥ 80% on modified modules
- ✅ Zero ruff/mypy violations
- ✅ Tests follow behavior-driven structure
- ✅ Fixtures eliminate code duplication
- ✅ User-focused test descriptions
- ✅ Minimal, strategic mocking

---

**Remember**: Write tests that make the codebase more maintainable, not just achieve coverage metrics.
