| 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.
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:
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
"""
🔧 Fixture Guidelines
Required Fixture Pattern
Create reusable fixtures that eliminate code duplication:
@pytest.fixture
def mock_vehicle_provider() -> MagicMock:
"""Fixture providing a mock vehicle components provider with realistic test data."""
provider = MagicMock()
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),
):
window = ComponentWindow(vehicle_components_provider=mock_vehicle_provider)
window.root = MagicMock()
return window
Fixture Design Rules
- One concern per fixture - Each fixture should have a single responsibility
- Realistic mock data - Use data that mirrors real system behavior
- Composable fixtures - Allow fixtures to depend on other fixtures
- Descriptive names - Fixture names should clearly indicate their purpose
- Minimal scope - Use the narrowest scope possible (function > class > module > session)
📝 Test Structure Standards
Test Class Organization
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
"""
Test Method Requirements
- Descriptive names -
test_user_can_select_template_by_double_clicking
- Single behavior focus - Test one behavior per method
- User-centric language - Write from the user's perspective
- 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
assert window.selected_template == "Copter/QuadX"
assert window.close_window.called_once()
assert mock_method.call_count == 1
🎭 Mocking Strategy
Minimal Mocking Principle
Only mock what is absolutely necessary:
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"
def test_template_selection(self) -> None:
with patch("tkinter.Tk"), \
patch("module.Class1"), \
patch("module.Class2"), \
patch("module.method1"), \
patch("module.method2"):
Mocking Guidelines
- Mock external dependencies (file system, network, databases)
- Mock UI framework calls (tkinter widgets, events)
- Don't mock the system under test - Test real behavior
- Use fixtures for complex mocks - Keep test methods clean
- Mock return values realistically - Match expected data types and structures
🏗️ Project-Specific Patterns
Frontend Tkinter Testing
def test_ui_behavior(self, template_overview_window_setup) -> None:
"""Test UI behavior without full window creation."""
def test_component_behavior(self, template_window) -> None:
"""Test component behavior with configured window."""
Backend/Logic Testing
def test_business_logic(self, mock_data_provider) -> None:
"""Test core logic with realistic data."""
@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:
- Never use
.update(): It forces a full event loop evaluation and will crash macOS.
Always use .update_idletasks() instead.
- 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.
@pytest.fixture
def safe_window_fixture(tk_root) -> Generator[MyWindow, None, None]:
with (
patch.object(tk.Toplevel, "winfo_fpixels", side_effect=tk.TclError("no display")),
patch("tkinter.Misc.update"),
patch("tkinter.Misc.update_idletasks"),
patch("tkinter.Misc.wait_visibility"),
patch("tkinter.Misc.wait_window"),
):
window = MyWindow(tk_root)
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
pytestmark = pytest.mark.gui
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 files follow specific naming conventions to clearly indicate their purpose and scope:
```text
tests/
├── test_frontend_tkinter_component.py
├── test_backend_logic.py
├── gui_*.py
├── integration_*.py
├── acceptance_*.py
├── unit_*.py
└── conftest.py
📊 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
🚀 Example: Complete Test Implementation
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
"""
template_window.tree.identify_row.return_value = "template_item"
template_window.tree.item.return_value = {"text": "Copter/QuadX"}
mock_event = MagicMock(y=100)
template_window._on_row_double_click(mock_event)
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
"""
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:
mock_event = MagicMock()
template_window._on_row_selection_change(mock_event)
template_window._update_selection()
mock_display.assert_called_once_with("Plane/FixedWing")
🛠️ Development Workflow
Pre-commit Requirements
- Run tests:
pytest tests/ -v
- Check coverage:
pytest --cov=ardupilot_methodic_configurator --cov-report=term-missing
- Format with ruff:
ruff format
- Lint with ruff:
ruff check --fix
- Type check with mypy:
mypy
- Advanced type check with pyright:
pyright
- Style check with pylint:
pylint $(git ls-files '*.py')
Test Execution Commands
pytest tests/ -v
pytest tests/ --cov=ardupilot_methodic_configurator --cov-report=html
pytest tests/test_frontend_tkinter_template_overview.py -v
pytest tests/ -k "test_user" -v
pytest tests/ --durations=10
Debugging Failed Tests
pytest tests/test_file.py::test_method -v -s
pytest tests/test_file.py::test_method --pdb
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.