* Implement template generalization for Entity/Location attribute editing Add AttributeEditContext pattern enabling ~95% code reuse between Entity and Location editing modals while maintaining type-safe owner-specific access. - Add AttributeEditContext base class and owner-specific implementations - Create generic edit_content_body.html template with extensible blocks - Add template filters/tags for dynamic method calls and URL generation - Update Entity/Location handlers to provide generalized context - Replace specific templates to extend generic version - Fix file upload and history restore template context issues All Location tests (139/139) and Entity editing tests (17/17) passing. * Updated documentation. Generic Attribute Editing, Testing * Added unit tests and fixed linting errors. * Rationalized view vs. edit mode names and directory organization. * Fixed JS issues with previous refactor. * More refactoring to rationalize view vs. edit mode. * Yet more refactoring to rationalize view vs. edit mode. * Continued refactoring to Javascript bug fixes for refactoring. * Fixed a couple unit tests and linting errors.
7.9 KiB
Testing Guidelines
Unit Tests
cd $PROJ_DIR/src
./manage.py test
High-Value vs Low-Value Testing Criteria
HIGH-VALUE Tests (Focus Here)
- Database constraints and cascade deletion behavior - Critical for data integrity
- Complex business logic and algorithms - Custom calculations, aggregation, processing
- Singleton pattern behavior - Manager classes, initialization, thread safety
- Enum property conversions with custom logic - from_name_safe(), business rules
- File handling and storage operations - Upload, deletion, cleanup, error handling
- Integration key parsing and external system interfaces - API boundaries
- Complex calculations - Geometric (SVG positioning), ordering, aggregation logic
- Caching and performance optimizations - TTL caches, database indexing
- Auto-discovery and Django startup integration - Module loading, initialization sequences
- Thread safety and concurrent operations - Locks, shared state, race conditions
- Background process coordination - Async/sync dual access, event loop management
LOW-VALUE Tests (Avoid These)
- Simple property getters/setters that just return field values
- Django ORM internals verification (Django already tests this)
- Trivial enum label checking without business logic
- Basic field access and obvious default values
- Simple string formatting without complex logic
Critical Testing Anti-Patterns (Never Do These)
NEVER Test Behavior Based on Log Messages
Problem: Log message assertions (self.assertLogs(), checking log output) are fragile and break easily when logging changes
Issue: Many existing tests deliberately disable logging for performance and clarity
Solution: Test actual behavior changes - state modifications, return values, method calls, side effects
# BAD - Testing based on log messages
with self.assertLogs('weather.manager', level='WARNING') as log_context:
manager.process_data(invalid_data)
self.assertTrue(any("Error processing" in msg for msg in log_context.output))
# GOOD - Testing actual behavior
mock_fallback = Mock()
with patch.object(manager, 'fallback_handler', mock_fallback):
result = manager.process_data(invalid_data)
mock_fallback.assert_called_once()
self.assertIsNone(result) # Verify expected failure behavior
NEVER Use a mock of a class when the real class is available
Mock-Centric Testing Instead of Behavior Testing
Problem: Tests focus on verifying mock calls rather than testing actual behavior and return values.
# BAD - Testing mock calls instead of behavior
@patch('module.external_service')
def test_process_data(self, mock_service):
mock_service.return_value = {'status': 'success'}
result = processor.process_data(input_data)
mock_service.assert_called_once_with(expected_params)
# Missing: What did process_data actually return?
# GOOD - Testing actual behavior and return values
@patch('module.external_service')
def test_process_data_returns_transformed_result(self, mock_service):
mock_service.return_value = {'status': 'success', 'data': 'raw_value'}
result = processor.process_data(input_data)
# Test the actual behavior and return value
self.assertEqual(result['transformed_data'], 'processed_raw_value')
self.assertEqual(result['status'], 'completed')
self.assertIn('timestamp', result)
Over-Mocking Internal Components
Problem: Mocking too many internal components breaks the integration between parts of the system.
# BAD - Mocking both HTTP layer AND internal converter
@patch('module.http_client.get')
@patch('module.DataConverter.parse')
def test_fetch_and_parse(self, mock_parse, mock_get):
mock_get.return_value = mock_response
mock_parse.return_value = mock_parsed_data
result = service.fetch_and_parse()
# This tests nothing about actual data flow
# GOOD - Mock only at system boundaries
@patch('module.http_client.get')
def test_fetch_and_parse_integration(self, mock_get):
mock_get.return_value = Mock(text='{"real": "json", "data": "here"}')
result = service.fetch_and_parse()
# Test that real data flows through real converter
self.assertIsInstance(result, ExpectedDataType)
self.assertEqual(result.parsed_field, "expected_value")
Additional Testing Anti-Patterns
Testing Implementation Details Instead of Interface Contracts
Problem: Tests verify internal implementation details rather than public interface behavior.
# BAD - Testing exact HTTP parameters instead of behavior
def test_api_call_constructs_correct_url(self):
client.make_request('entity_123')
expected_url = 'https://api.service.com/v1/entities/entity_123'
expected_headers = {'Authorization': 'Bearer token', 'Content-Type': 'application/json'}
mock_post.assert_called_once_with(expected_url, headers=expected_headers)
# GOOD - Testing the interface contract
def test_api_call_returns_entity_data(self):
mock_response_data = {'id': 'entity_123', 'name': 'Test Entity'}
mock_post.return_value = Mock(json=lambda: mock_response_data)
result = client.make_request('entity_123')
# Test the contract: what the method promises to return
self.assertEqual(result['id'], 'entity_123')
self.assertEqual(result['name'], 'Test Entity')
Testing Best Practices Summary
- Mock at system boundaries only (HTTP calls, database, external services)
- Test return values and state changes, not mock call parameters
- Use real data through real code paths when possible
- Test error messages provide useful context for debugging
- Focus on interface contracts, not implementation details
- Create focused tests that test one behavior well
- Test meaningful edge cases that affect business logic
- Verify data transformations work correctly end-to-end
- Use real database operations over ORM mocking when testing business logic
- Test database state changes rather than mocking ORM calls to verify actual behavior
Database vs Mock Testing Strategy
Prefer Real Database Operations:
- Database state verification tests actual business logic and relationships
- Cascade deletion, constraints, and indexing are critical system behaviors
- TransactionTestCase provides proper isolation for database-dependent tests
- Real data flows through real code paths reveal integration issues
When to Mock vs Real Database:
- Mock external APIs (HTTP calls, third-party services)
- Use real database for business logic, relationships, and data transformations
- Mock at system boundaries, not internal ORM operations
Django-Specific Testing Patterns
See Testing Patterns for detailed Django testing patterns including:
- Abstract Model Testing
- Integration Key Pattern Testing
- Singleton Manager Testing
- Background Process and Threading Testing
- Manager Class Async/Sync Testing
Development Data Injection
The development data injection system provides a runtime mechanism to modify application behavior without code changes or Django restarts. This is useful for testing scenarios that would otherwise require complex backend state setup.
Example use case: Injecting pre-formatted status responses for UI testing - you can override the /api/status endpoint to return specific transient view suggestions, allowing you to test auto-view switching behavior without manipulating the actual backend systems.
For complete usage details, see: hi.testing.dev_injection.DevInjectionManager
Related Documentation
- Django-specific patterns: Testing Patterns
- Test data management: Test Data Management
- Simulator testing: Test Simulator
- Coding standards: Coding Standards
- View testing: Frontend Guidelines
- Examples: Test Case Examples