Skip to main content
Skip to main content

Testing

FilterMate has a comprehensive test suite covering utilities, backends, UI components, and integration scenarios. The test suite targets 80%+ code coverage and uses mocked QGIS modules for fast, isolated testing.

Overview​

Test Categories​

FilterMate's test suite is organized into several categories:

CategoryFilesPurpose
Backend Teststest_backends.pyMulti-backend architecture, factory pattern
Utility Teststest_appUtils.py, test_constants.pyCore utility functions
UI Teststest_ui_*.pyUI configuration, themes, styles
Integration Teststest_filter_history_integration.pyFeature interaction
Performance Teststest_performance.py, benchmark_simple.pySpeed benchmarks
Refactoring Teststest_refactored_helpers_*.pyHelper method coverage
Signal Teststest_signal_utils.pySignal/slot management
Configuration Teststest_config_json_reactivity.py, test_choices_type_config.pyDynamic config

Test Statistics​

  • Total Test Files: 35+
  • Test Cases: 150+
  • Target Coverage: 80%
  • Current Coverage: ~75%
  • Mock Framework: pytest-mock + pytest-qt

Setup​

Prerequisites​

  1. Python 3.7+ (QGIS Python environment)
  2. pip package manager
  3. No QGIS installation required (tests use mocks)

Install Test Dependencies​

Method 1: Using requirements.txt​

# From project root
cd /path/to/filter_mate
pip install -r tests/requirements-test.txt

Method 2: Manual Installation​

pip install pytest pytest-cov pytest-mock pytest-qt

Dependencies Explained​

PackagePurpose
pytestTest framework and runner
pytest-covCode coverage reporting
pytest-mockMocking QGIS and external dependencies
pytest-qtQt signal/slot testing

Running Tests​

Basic Commands​

Run All Tests​

# From project root
pytest tests/

# With verbose output
pytest tests/ -v

# Show print statements
pytest tests/ -v -s

Run Specific Test File​

# Test backends
pytest tests/test_backends.py -v

# Test utilities
pytest tests/test_appUtils.py -v

# Test UI configuration
pytest tests/test_ui_config.py -v

Run Specific Test Class​

# Test only backend factory
pytest tests/test_backends.py::TestBackendFactory -v

# Test only signal utilities
pytest tests/test_signal_utils.py::TestSignalUtils -v

Run Specific Test Function​

# Test specific function
pytest tests/test_backends.py::TestBackendFactory::test_get_backend_postgresql -v

# Test with keyword filter
pytest tests/ -k "backend" -v

Coverage Reports​

Generate HTML Coverage Report​

# Run tests with coverage
pytest tests/ --cov=modules --cov=filter_mate_app --cov-report=html

# Open report
# Windows:
start htmlcov/index.html

# Linux/Mac:
xdg-open htmlcov/index.html # Linux
open htmlcov/index.html # Mac

Generate Terminal Coverage Report​

# Brief coverage summary
pytest tests/ --cov=modules --cov=filter_mate_app

# Detailed line-by-line report
pytest tests/ --cov=modules --cov=filter_mate_app --cov-report=term-missing

Generate XML Coverage (CI/CD)​

# For CI/CD integration
pytest tests/ --cov=modules --cov=filter_mate_app --cov-report=xml

Test Structure​

Directory Layout​

tests/
β”œβ”€β”€ conftest.py # Shared fixtures & QGIS mocks
β”œβ”€β”€ requirements-test.txt # Test dependencies
β”‚
β”œβ”€β”€ Backend Tests
β”‚ └── test_backends.py # Backend architecture
β”‚
β”œβ”€β”€ Core Utility Tests
β”‚ β”œβ”€β”€ test_appUtils.py # Database utilities
β”‚ β”œβ”€β”€ test_constants.py # Module constants
β”‚ └── test_signal_utils.py # Signal management
β”‚
β”œβ”€β”€ UI Tests
β”‚ β”œβ”€β”€ test_ui_config.py # UI profile detection
β”‚ β”œβ”€β”€ test_ui_config_ratios.py # Dimension calculations
β”‚ β”œβ”€β”€ test_ui_styles.py # Theme application
β”‚ β”œβ”€β”€ test_qt_json_view_themes.py # JSON view themes
β”‚ └── test_theme_detection.py # Theme synchronization
β”‚
β”œβ”€β”€ Configuration Tests
β”‚ β”œβ”€β”€ test_config_json_reactivity.py # Dynamic config updates
β”‚ β”œβ”€β”€ test_choices_type_config.py # ChoicesType enum
β”‚ └── test_color_contrast.py # Accessibility compliance
β”‚
β”œβ”€β”€ Backend-Specific Tests
β”‚ β”œβ”€β”€ test_spatialite_expression_quotes.py # Field name quoting
β”‚ β”œβ”€β”€ test_spatialite_temp_table_fix.py # Temp table handling
β”‚ β”œβ”€β”€ test_sqlite_lock_handling.py # Database locking
β”‚ β”œβ”€β”€ test_geometry_repair.py # Auto-repair invalid geometries
β”‚ β”œβ”€β”€ test_buffer_error_handling.py # Buffer operation errors
β”‚ β”œβ”€β”€ test_ogr_type_handling.py # OGR provider detection
β”‚ └── test_geopackage_detection.py # GeoPackage identification
β”‚
β”œβ”€β”€ Feature Tests
β”‚ β”œβ”€β”€ test_filter_history.py # History tracking
β”‚ β”œβ”€β”€ test_filter_history_integration.py # UI integration
β”‚ β”œβ”€β”€ test_feedback_utils.py # User messaging
β”‚ └── test_prepared_statements.py # SQL preparation
β”‚
β”œβ”€β”€ Performance Tests
β”‚ β”œβ”€β”€ test_performance.py # Speed benchmarks
β”‚ └── benchmark_simple.py # Quick profiling
β”‚
β”œβ”€β”€ Refactoring Tests
β”‚ β”œβ”€β”€ test_refactored_helpers_appTasks.py # 58 helper methods
β”‚ └── test_refactored_helpers_dockwidget.py # 14 UI helpers
β”‚
└── Migration Tests
β”œβ”€β”€ test_layer_provider_type_migration.py # Provider type handling
β”œβ”€β”€ test_source_table_name.py # Table name extraction
└── test_buffer_type.py # Buffer type validation

Key Files Explained​

conftest.py​

Provides shared test fixtures and QGIS mocks:

@pytest.fixture
def mock_qgs_layer():
"""Mock QgsVectorLayer for testing"""
layer = Mock()
layer.providerType.return_value = 'postgres'
layer.featureCount.return_value = 1000
return layer

@pytest.fixture
def mock_iface():
"""Mock QGIS interface"""
iface = Mock()
iface.messageBar.return_value = Mock()
return iface

test_backends.py​

Tests the multi-backend architecture:

class TestBackendFactory:
def test_get_backend_postgresql(self):
"""Test PostgreSQL backend selection"""
# Arrange
layer = create_postgres_layer()

# Act
backend = BackendFactory.get_backend(layer)

# Assert
assert isinstance(backend, PostgresqlBackend)
assert backend.supports_predicates()

Testing Workflow​

Test-Driven Development (TDD)​

Continuous Integration​

  1. Pre-commit: Run fast tests locally
  2. Push: Trigger CI pipeline
  3. CI: Run full test suite with coverage
  4. Review: Check coverage reports
  5. Merge: Only if tests pass + coverage maintained

Writing Tests​

Test Template​

import pytest
from unittest.mock import Mock, patch

class TestMyFeature:
"""Test suite for MyFeature"""

@pytest.fixture
def setup_data(self):
"""Setup test data"""
return {
'param1': 'value1',
'param2': 123
}

def test_basic_functionality(self, setup_data):
"""Test basic feature operation"""
# Arrange
input_data = setup_data
expected_output = 'expected_value'

# Act
result = my_function(input_data)

# Assert
assert result == expected_output

def test_error_handling(self):
"""Test error scenarios"""
# Arrange
invalid_input = None

# Act & Assert
with pytest.raises(ValueError):
my_function(invalid_input)

@patch('modules.appUtils.psycopg2')
def test_with_mock(self, mock_psycopg2):
"""Test with external dependency mocked"""
# Arrange
mock_psycopg2.connect.return_value = Mock()

# Act
result = function_using_psycopg2()

# Assert
mock_psycopg2.connect.assert_called_once()
assert result is not None

Best Practices​

βœ… Do's​

  • βœ… Use descriptive names - test_filter_applies_buffer_correctly
  • βœ… Follow AAA pattern - Arrange, Act, Assert
  • βœ… One assertion per test (when possible)
  • βœ… Mock external dependencies - QGIS, databases, filesystem
  • βœ… Test edge cases - Empty inputs, None values, large datasets
  • βœ… Use fixtures - Share setup code across tests
  • βœ… Document test purpose - Clear docstrings

❌ Don'ts​

  • ❌ Don't test QGIS internals - Only test FilterMate code
  • ❌ Don't use real databases - Mock all DB operations
  • ❌ Don't depend on test order - Tests should be independent
  • ❌ Don't skip cleanup - Use fixtures with yield
  • ❌ Don't write brittle tests - Avoid hardcoded values
  • ❌ Don't ignore failures - Fix or document intentional skips

Testing Checklist​

When adding new code, ensure tests cover:

  • Happy path - Normal expected usage
  • Edge cases - Boundary conditions
  • Error handling - Invalid inputs, exceptions
  • Integration - Interaction with other components
  • Performance - Not too slow (if critical path)
  • Mocking - External dependencies isolated

Test Examples​

Example 1: Testing Utility Function​

# tests/test_appUtils.py
def test_get_primary_key_name_postgres(mock_postgres_layer):
"""Test primary key extraction from PostgreSQL layer"""
# Arrange
mock_postgres_layer.dataProvider().uri().table.return_value = 'my_table'
mock_postgres_layer.dataProvider().uri().keyColumn.return_value = 'id'

# Act
pk_name = get_primary_key_name(mock_postgres_layer)

# Assert
assert pk_name == 'id'

Example 2: Testing Backend Selection​

# tests/test_backends.py
class TestBackendFactory:
def test_recommend_backend_large_dataset(self):
"""Should recommend PostgreSQL for large datasets"""
# Arrange
layer = Mock()
layer.featureCount.return_value = 100000
layer.providerType.return_value = 'postgres'

# Act
recommendation = BackendFactory.recommend_backend(layer)

# Assert
assert recommendation == 'postgresql'

Example 3: Testing UI Configuration​

# tests/test_ui_config.py
def test_detect_ui_profile_compact():
"""Should detect compact profile for small screens"""
# Arrange
screen_width = 1366
screen_height = 768

# Act
profile = detect_ui_profile(screen_width, screen_height)

# Assert
assert profile == 'compact'

Example 4: Testing Signal Handling​

# tests/test_signal_utils.py
def test_safe_disconnect_existing_signal(qtbot):
"""Should disconnect existing signal without error"""
# Arrange
widget = QPushButton()
callback = Mock()
widget.clicked.connect(callback)

# Act
safe_disconnect(widget.clicked, callback)
widget.click()

# Assert
callback.assert_not_called() # Signal disconnected

Troubleshooting​

Common Issues​

Import Errors​

Problem: ModuleNotFoundError: No module named 'qgis'

Solution: Tests use mocked QGIS modules. Ensure conftest.py is in place:

# conftest.py must contain QGIS mocks
import sys
from unittest.mock import Mock

sys.modules['qgis'] = Mock()
sys.modules['qgis.core'] = Mock()
# ... more mocks

Test Discovery Issues​

Problem: pytest doesn't find tests

Solution: Ensure proper naming:

  • Test files: test_*.py
  • Test classes: class Test*
  • Test functions: def test_*()

Fixture Not Found​

Problem: fixture 'my_fixture' not found

Solution: Check fixture is in conftest.py or same file:

# conftest.py
@pytest.fixture
def my_fixture():
return "test_data"

Coverage Too Low​

Problem: Coverage report shows < 80%

Solution:

  1. Identify uncovered lines: pytest --cov --cov-report=term-missing
  2. Add tests for uncovered code
  3. Use # pragma: no cover for untestable code (sparingly)

Debugging Tests​

Use pytest verbosity​

# Show test names and results
pytest tests/test_backends.py -v

# Show print statements
pytest tests/test_backends.py -v -s

# Stop at first failure
pytest tests/test_backends.py -x

Use pytest debugger​

# Drop into debugger on failure
pytest tests/test_backends.py --pdb

# Set breakpoint in code
import pdb; pdb.set_trace()

Check mock calls​

# Verify mock was called
mock_function.assert_called()
mock_function.assert_called_once()
mock_function.assert_called_with(expected_arg)

# Print mock calls for debugging
print(mock_function.call_args_list)

CI/CD Integration​

GitHub Actions Example​

# .github/workflows/test.yml
name: Run Tests

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'

- name: Install dependencies
run: |
pip install -r tests/requirements-test.txt

- name: Run tests with coverage
run: |
pytest tests/ --cov=modules --cov=filter_mate_app --cov-report=xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
file: ./coverage.xml

Performance Testing​

Benchmark Tests​

FilterMate includes performance benchmarks:

# Run performance tests
pytest tests/test_performance.py -v

# Run simple benchmark
python tests/benchmark_simple.py

Performance Goals​

OperationTarget TimeBackend
Filter 10k features< 1sAny
Filter 100k features< 5sPostgreSQL
Export 10k features< 2sAny
UI update< 100msN/A

Contributing Tests​

When contributing to FilterMate:

  1. Write tests first (TDD approach)
  2. Maintain coverage (β‰₯ 80%)
  3. Follow naming conventions
  4. Mock external dependencies
  5. Document test purpose
  6. Run full suite before PR

Test PR Checklist​

  • All tests pass locally
  • New features have tests
  • Coverage maintained or improved
  • No skipped tests without reason
  • Tests run in < 60 seconds
  • Documentation updated

See Also​

Resources​


Last updated: December 8, 2025