* Document design workflow process and update CLAUDE.md
- Add comprehensive design workflow documentation in docs/dev/workflow/design-workflow.md
- Update CLAUDE.md with design work documentation workflow section
- Establishes data/design/issue-{number}/ pattern for local work
- Documents GitHub issue attachment vs comment organization
- Provides reusable process for future design-focused issues
- Maintains repository cleanliness while enabling design iteration
* Snapshpt fo semi-working new entity and attribute edit modal.
* Refactored new forms to remove duplication. Fixed some textarea bugs.
* Fixed textarea attributes to prevent data corruption.
* Fixed new attribute validation error handling.
* Moved Entity attribute formset non-file filtering closer to def.
* Styling impovements.
* Added attribute value history browsing and restoring.
* Fixed styling on new entity edit modal header area.
* Refactored the messy and confusing new entity edit modal.
* Fixed icon in new attruibute form.
* Attribute Edit style changes.
* Fixed bad claude factoring. Fixed CSRF file upload issue.
* Added scroll-to in antinode.js whic helped fix file upload UX issue.
* Fixed styling of the add new attribute card for the 'mark as secret'.
* Added modified field forms styling for new entity edit modal.
* Fixed bug in secret attribute readonly editing logic.
* Added file attribute value editing to new entity edit modal.
* Removed legacy EntityEditView and related code.
* Refactor to remove EntityEditData.
* Refactor to rename: EntityDetailsData -> EntityEditModeData
* More refactoring for name change from "details" to "edit mode"
* Removed debug log messages. Doc typo fix.
* Refactored entity views to add helper classes.
* Coded cleanup and test fixes.
* Refactored to replace use of hardcoded DOM ids, classes, selectors.
* Refactorings: better naming removed debug messages.
* Renamed "property" to "attribute".
* Fixed unit test gaps.
* Replaced hardcoded form field prefixes with common access.
* Added EntityTransitionType to replace brittle "magic" strings.
* Tweaks on display names for entity edit modal.
* Added missing __init__.py to nested test dirs. (New failures found.)
* Working on fixing unit tests: checkpoint. WIP
* Fixed units tests
* Removed mocking from soem unit tests.
* Removed partial V2 implementation mistake from entity edit work.
* Added testing doc about lessons learned.
* Added a bunch of testing-related documentation.
* Fixed test expectation for LocationAttributeUploadView invalid upload
Updated test to expect 200 status with modal form response instead of 400 error, aligning with new form error handling behavior from entity attribute redesign.
10 KiB
Django Testing Anti-Patterns and Lessons Learned
Overview
This document captures critical discoveries and learnings from a comprehensive effort to fix over-mocking issues in Django unit tests across the HI application. The work involved systematically converting heavily mocked tests to use real objects, revealing numerous anti-patterns and best practices.
Core Anti-Patterns Identified
1. Over-Mocking Database Operations
Problem: Tests were mocking fundamental Django operations like model creation, relationships, and queries instead of using real database objects in test transactions.
Examples Found:
# Anti-pattern: Mocking basic model operations
@patch.object(CollectionManager, 'toggle_entity_in_collection')
@patch.object(CollectionManager, 'get_collection_data')
def test_post_toggle_entity_add(self, mock_toggle, mock_get_data):
mock_toggle.return_value = True
Solution:
# Better: Test real database relationships
def test_post_toggle_entity_add(self):
# Verify entity is not in collection initially
self.assertFalse(CollectionEntity.objects.filter(collection=self.collection, entity=self.entity).exists())
response = self.client.post(url)
# Verify entity was added to collection
self.assertTrue(CollectionEntity.objects.filter(collection=self.collection, entity=self.entity).exists())
Lesson: Django's test framework provides isolated database transactions. Use real objects instead of mocking database operations.
2. Mocking View Delegation Instead of Testing Integration
Problem: Tests mocked entire view classes when testing view delegation, missing integration issues and actual functionality.
Example:
# Anti-pattern: Mocking the delegated view
@patch('hi.apps.edit.views.CollectionReorderEntitiesView')
def test_reorder_entities_in_collection(self, mock_view_class):
mock_view = mock_view_class.return_value
mock_view.post.return_value = JsonResponse({'status': 'ok'})
Solution:
# Better: Test the full integration
def test_reorder_entities_in_collection(self):
# Create real entities and add to collection
CollectionEntity.objects.create(collection=self.collection, entity=entity1, order_id=0)
# Test actual reordering
response = self.client.post(url, post_data)
# Verify database changes
reordered_entities = list(CollectionEntity.objects.filter(collection=self.collection).order_by('order_id'))
self.assertEqual(reordered_entities[0].entity, entity3) # Verify new order
Lesson: Test the full integration path rather than just delegation mechanics.
3. Incorrect Response Type Expectations
Problem: Tests expected traditional HTTP redirects (302) when views actually returned JSON responses (200) via antinode.js integration.
Example:
# Anti-pattern: Wrong response expectation
def test_post_delete_with_confirmation(self):
response = self.client.post(url, {'action': 'confirm'})
self.assertEqual(response.status_code, 302) # Wrong!
Solution:
# Better: Expect JSON responses from antinode views
def test_post_delete_with_confirmation(self):
response = self.client.post(url, {'action': 'confirm'})
self.assertSuccessResponse(response)
self.assertJsonResponse(response)
data = response.json()
self.assertEqual(data['location'], expected_url)
Lesson: Understand your application's AJAX/async response patterns. Modern web apps often return JSON instead of traditional redirects.
Model Relationship Discoveries
Many-to-Many Through Models
Discovery: Collections and entities have a many-to-many relationship through CollectionEntity, not a direct relationship.
Wrong Assumption:
# This doesn't work - no direct relationship
self.collection.entities.add(self.entity)
Correct Approach:
# Use the through model
from hi.apps.collection.models import CollectionEntity
CollectionEntity.objects.create(collection=self.collection, entity=self.entity)
Lesson: Always verify model relationships in Django admin or model definitions before writing tests.
Entity Pairing System
Discovery: Entity pairings work through EntityStateDelegation objects, not direct relationships.
Implementation:
from hi.apps.entity.models import EntityStateDelegation
for entity_state in self.entity.states.all():
EntityStateDelegation.objects.create(
entity_state=entity_state,
delegate_entity=self.paired_entity
)
Lesson: Complex business logic often involves intermediate models that aren't immediately obvious.
Form Validation Requirements
Missing Required Fields
Discovery: LocationItemPositionForm requires all position fields (svg_x, svg_y, svg_scale, svg_rotate), not just coordinates.
Problem:
# Incomplete form data
response = self.client.post(url, {
'svg_x': '60.0',
'svg_y': '70.0' # Missing svg_scale and svg_rotate
})
# Position wasn't updated in database
Solution:
# Complete form data
response = self.client.post(url, {
'svg_x': '60.0',
'svg_y': '70.0',
'svg_scale': '1.0',
'svg_rotate': '0.0'
})
Lesson: Always check form field requirements. Use Django form validation errors to debug incomplete test data.
Session and Middleware Requirements
View Parameter Dependencies
Discovery: Many views depend on session state managed by custom middleware for location, collection, and view mode context.
Common Setup Pattern:
def setUp(self):
super().setUp()
# Set edit mode (required by decorator)
self.setSessionViewMode(ViewMode.EDIT)
# Create location/location view for middleware
location = Location.objects.create(...)
location_view = LocationView.objects.create(location=location, ...)
# Set collection context if needed
self.setSessionCollection(self.collection)
Lesson: Understand your application's middleware dependencies. Views may require specific session state to function.
Default Object Resolution
Discovery: Views often use "get_default" patterns that require objects with specific order_id values.
Pattern:
# Ensure this is the default by setting order_id=0
self.collection.order_id = 0
self.collection.save()
Lesson: Many applications use ordering conventions for default object resolution.
Test Infrastructure Patterns
Synthetic Data Pattern
Discovery: The application uses a consistent synthetic data pattern across modules for test data creation.
Structure:
# hi/apps/{module}/tests/synthetic_data.py
class {Module}SyntheticData:
@staticmethod
def create_test_{model}(**kwargs) -> {Model}:
unique_id = str(uuid.uuid4())[:8]
defaults = {
'name': f'Test {Model} {unique_id}',
# ... reasonable defaults
}
defaults.update(kwargs)
return {Model}.objects.create(**defaults)
Lesson: Centralized test data creation prevents duplication and ensures consistency.
Test Base Class Hierarchy
Discovery: The application uses specialized test base classes with different capabilities.
Classes Found:
SyncViewTestCase: For traditional synchronous viewsDualModeViewTestCase: For views that work in both sync and async modes- Custom assertion methods:
assertJsonResponse(),assertHtmlResponse(),assertSuccessResponse()
Lesson: Leverage application-specific test utilities rather than recreating common patterns.
Enum and String Conversion Patterns
LabeledEnum Usage
Discovery: The application uses LabeledEnum types that require string conversion with str().
Pattern:
# Always use str() conversion for enum fields
entity = Entity.objects.create(
entity_type_str=str(EntityType.LIGHT) # Not just EntityType.LIGHT
)
Lesson: Understand your application's enum handling patterns.
HTML ID Format Conventions
Discovery: The application uses specific HTML ID formats for different item types.
Pattern:
# Use ItemType.parse_html_id() format: hi-{type}-{id}
'hi-entity-1' # Entity with ID 1
'hi-collection-1' # Collection with ID 1
'hi-location_view-1' # LocationView with ID 1 (note underscore)
Lesson: Web applications often have specific ID conventions for JavaScript integration.
Error Handling and Exception Patterns
Custom Exception Handling
Discovery: The application uses custom exceptions handled by middleware rather than Django's built-in HTTP exceptions.
Pattern:
# Application uses custom MethodNotAllowedError
# Middleware converts to HttpResponseNotAllowed(405)
Lesson: Understand your application's exception handling architecture.
Best Practices Derived
1. Test Real Behavior, Not Implementation Details
- Test database state changes, not method calls
- Test full request-response cycles, not just view delegation
- Verify business logic outcomes, not internal mechanics
2. Understand Your Application Architecture
- Know your middleware dependencies
- Understand session management patterns
- Learn enum and string conversion requirements
- Study model relationship patterns
3. Use Application-Specific Test Utilities
- Leverage existing test base classes
- Use synthetic data generators
- Employ custom assertion methods
- Follow established test data patterns
4. Debug Systematically
- Read error messages carefully - they often reveal missing setup
- Check form validation requirements when database changes don't occur
- Verify response types match expectations (JSON vs HTML vs redirects)
- Use
print()debugging in tests to understand data flow
5. Create Missing Test Infrastructure
- Build synthetic data generators following established patterns
- Create reusable test setup methods
- Document model relationship requirements
- Share learnings with the team
Conclusion
The transition from over-mocked tests to real object testing revealed deep application architecture patterns and significantly improved test coverage quality. The key insight is that Django's test framework is designed to support real database operations in isolated transactions, making mocking of basic CRUD operations unnecessary and counterproductive.
These patterns and lessons should guide future test development to avoid the anti-patterns that led to fragile, hard-to-maintain tests that provided false confidence in system behavior.