mirror of
https://github.com/cassandra/home-information.git
synced 2026-04-18 21:49:16 -04:00
* Added new home profile icon images for upcoming new start page. * New start page design and structure. New profiles app created (stub). * First time sidebars/modals. ProfileManager JSON docs and parsing. * First working ProfileManager. WIP * Added mechanisms for knowing when to show first-time help content. * Refactor to clean up initial flows and intro help display. * New DevTools views structure. Added stub for profile snapshot tool. * Created initial profiel snapshot generator devtool. * Rationalized profile generation and parsing with shared constants. * Fixed location SVG path issues in predefined profiles. * Improved unit tests for ProfileManager. * Added SVG fragment install and generation for predefined profiles. * Fixed unit tests depending on MEDIA_ROOT. * Fixed remaining unit tests for MEDIA_ROOT isolation. * Moved JSON initialization profiles to 'assets' dir. * Refactored all the help content and added view mode ref dialog.
11 KiB
11 KiB
Testing Patterns
Django View Testing
Django views in this application come in five distinct patterns that require different testing approaches:
- Synchronous HTML Views - Traditional Django page views returning HTML responses
- Synchronous JSON Views - API endpoints returning JSON responses
- Asynchronous HTML Views - AJAX views returning HTML snippets for DOM insertion
- Asynchronous JSON Views - AJAX views returning JSON data for JavaScript processing
- Dual-Mode Views - Views (HiModalView/HiGridView) that handle both sync and async requests
View Testing Base Classes
The framework uses a mixin-based architecture to provide clean separation of concerns:
ViewTestBase- Common utilities and core assertionsSyncTestMixin- Synchronous testing capabilities (regularclient.get(),client.post())AsyncTestMixin- Asynchronous testing capabilities (async_get(),async_post()with AJAX headers)
Use these base classes from hi.tests.view_test_base:
from django.urls import reverse
from hi.tests.view_test_base import SyncViewTestCase, AsyncViewTestCase, DualModeViewTestCase
class TestMySyncViews(SyncViewTestCase):
def test_synchronous_html_view(self):
url = reverse('my_view_name')
response = self.client.get(url)
self.assertSuccessResponse(response)
self.assertHtmlResponse(response)
self.assertTemplateRendered(response, 'my_app/template.html')
class TestMyAsyncViews(AsyncViewTestCase):
def test_asynchronous_html_view(self):
url = reverse('my_async_view_name')
response = self.async_get(url) # Automatically includes AJAX headers
self.assertSuccessResponse(response)
self.assertJsonResponse(response)
class TestMyDualModeViews(DualModeViewTestCase):
def test_view_synchronous_mode(self):
url = reverse('my_dual_view_name')
response = self.client.get(url)
self.assertSuccessResponse(response)
self.assertHtmlResponse(response)
def test_view_asynchronous_mode(self):
url = reverse('my_dual_view_name')
response = self.async_get(url)
self.assertSuccessResponse(response)
self.assertJsonResponse(response)
Helper Methods
Status Code Assertions:
assertResponseStatusCode(response, expected_code)- Verifies specific status codeassertSuccessResponse(response)- Verifies 2xx status codeassertErrorResponse(response)- Verifies 4xx status codeassertServerErrorResponse(response)- Verifies 5xx status code
Response Type Assertions:
assertHtmlResponse(response)- Verifies HTML content typeassertJsonResponse(response)- Verifies JSON content type
Template Assertions:
assertTemplateRendered(response, template_name)- Verifies specific template was used
Session Assertions:
assertSessionValue(response, key, expected_value)- Verifies session contains specific key-valueassertSessionContains(response, key)- Verifies session contains specific key
Session Management:
setSessionViewType(view_type)- Set ViewType in sessionsetSessionViewMode(view_mode)- Set ViewMode in sessionsetSessionLocationView(location_view)- Set location_view_id in sessionsetSessionCollection(collection)- Set collection_id in sessionsetSessionViewParameters(view_type=None, view_mode=None, location_view=None, collection=None)
Redirect Testing:
assertRedirectsToTemplates(initial_url, expected_templates)- Follow redirects and verify final templates
AJAX Request Methods:
async_get(url, data=None)- GET request with AJAX headersasync_post(url, data=None)- POST request with AJAX headersasync_put(url, data=None)- PUT request with AJAX headersasync_delete(url, data=None)- DELETE request with AJAX headers
Manager Class Async/Sync Testing
Many manager classes in this codebase follow a dual sync/async pattern to support both traditional Django views and async integration services.
Manager Pattern Characteristics
- Singleton pattern with
__init_singleton__() - Both sync
ensure_initialized()and async initialization methods - Mix of sync methods for Django ORM access and async methods for integration services
- Thread safety considerations and shared state management
Async Testing Infrastructure
class AsyncManagerTestCase(TransactionTestCase):
"""Base class for async manager tests with proper infrastructure."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create a single shared event loop for all tests in this class
cls._test_loop = asyncio.new_event_loop()
asyncio.set_event_loop(cls._test_loop)
@classmethod
def tearDownClass(cls):
if hasattr(cls, '_test_loop'):
cls._test_loop.close()
super().tearDownClass()
def run_async(self, coro):
"""Helper method to run async coroutines using the shared event loop."""
return self._test_loop.run_until_complete(coro)
def setUp(self):
super().setUp()
# Reset singleton state for each test
ManagerClass._instances = {}
self.manager = ManagerClass()
# Clear any cached state
if hasattr(self.manager, '_recent_transitions'):
self.manager._recent_transitions.clear()
def test_async_method(self):
"""Example async test using wrapper pattern."""
async def async_test_logic():
# Use sync_to_async for database operations
entity = await sync_to_async(Entity.objects.create)(name='Test Entity')
result = await self.manager.async_method(entity)
self.assertIsNotNone(result)
self.run_async(async_test_logic())
Key Requirements:
- Use
TransactionTestCaseinstead ofBaseTestCaseto avoid database locking - Shared event loop prevents SQLite concurrency issues with multiple async tests
- Reset singleton state between tests to ensure isolation
- Wrap sync database operations with
sync_to_async()in async test code - Use
select_related()in manager code to prevent lazy loading in async contexts
Critical ORM Access Pattern:
# In manager async methods - avoid lazy loading issues
event_clauses = await sync_to_async(list)(
event_definition.event_clauses.select_related('entity_state').all()
)
# In tests - wrap database operations
entity = await sync_to_async(Entity.objects.create)(name='Test')
Django-Specific Testing Patterns
Abstract Model Testing
# Create concrete test class for abstract models
class ConcreteTestModel(AbstractModel):
def required_abstract_method(self):
return "test_implementation"
# Mock Django operations for database-less testing
with patch('django.db.models.Model.save') as mock_save:
instance.save()
mock_save.assert_called_once()
Integration Key Pattern Testing
def test_integration_key_inheritance(self):
model = TestModel.objects.create(
integration_id='test_id',
integration_name='test_integration'
)
self.assertEqual(model.integration_id, 'test_id')
Singleton Manager Testing
def test_manager_singleton_behavior(self):
manager1 = ManagerClass()
manager2 = ManagerClass()
self.assertIs(manager1, manager2)
Background Process and Threading Testing
async def test_async_manager_method(self):
with patch('asyncio.run') as mock_run:
result = await manager.async_method()
mock_run.assert_called()
def test_manager_thread_safety(self):
results = []
def worker():
results.append(manager.thread_safe_operation())
threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
Authentication and Permission Testing
def test_protected_view_requires_authentication(self):
url = reverse('protected_view')
response = self.client.get(url)
login_url = reverse('login')
self.assertRedirects(response, f'{login_url}?next={url}')
def test_protected_view_with_authenticated_user(self):
self.client.force_login(self.user)
url = reverse('protected_view')
response = self.client.get(url)
self.assertSuccessResponse(response)
self.assertHtmlResponse(response)
Form Validation Testing
def test_form_validation_success(self):
form_data = {'name': 'Test Entity', 'location': self.location.id}
url = reverse('entity_create')
response = self.client.post(url, form_data)
success_url = reverse('entity_list')
self.assertRedirects(response, success_url)
self.assertTrue(Entity.objects.filter(name='Test Entity').exists())
def test_form_validation_errors(self):
form_data = {'name': ''} # Missing required field
url = reverse('entity_create')
response = self.client.post(url, form_data)
self.assertSuccessResponse(response) # Form errors return 200, not 4xx
self.assertHtmlResponse(response)
self.assertTemplateRendered(response, 'entity/create.html')
self.assertFormError(response, 'form', 'name', 'This field is required.')
Database Setup for Tests
View tests should create real test data to verify complete request/response flows:
def setUp(self):
super().setUp()
self.location = Location.objects.create(name='Test Location')
self.entity = Entity.objects.create(
integration_id='test.entity',
integration_name='test_integration',
location=self.location
)
def test_location_view_with_entities(self):
url = reverse('location_detail', kwargs={'location_id': self.location.id})
response = self.client.get(url)
self.assertSuccessResponse(response)
self.assertHtmlResponse(response)
self.assertTemplateRendered(response, 'location/detail.html')
self.assertEqual(response.context['location'], self.location)
self.assertIn(self.entity, response.context['entities'])
Testing Code needing MEDIA_ROOT
# Use context manager for individual test methods
def test_file_upload(self):
with self.isolated_media_root() as temp_media_root:
# File operations here
test_file = self.create_test_text_file("test.txt", "content")
# ... test logic
# OR use class-level isolation for test classes with many file operations
class TestFileOperations(BaseTestCase):
def setUp(self):
super().setUp()
self._temp_media_dir = tempfile.mkdtemp()
self._settings_patcher = override_settings(MEDIA_ROOT=self._temp_media_dir)
self._settings_patcher.enable()
def tearDown(self):
if hasattr(self, '_settings_patcher'):
self._settings_patcher.disable()
if hasattr(self, '_temp_media_dir'):
shutil.rmtree(self._temp_media_dir, ignore_errors=True)
super().tearDown()
Related Documentation
- Testing guidelines: Testing Guidelines
- UI testing: UI Testing
- Backend testing: Backend Guidelines