Files
home-information/docs/dev/testing/testing-lessons-learned.md
Tony C 193c86eb65 Entity View/Edit Modal Redesign (#136)
* 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.
2025-08-31 13:21:22 -05:00

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 views
  • DualModeViewTestCase: 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.