mirror of
https://github.com/cassandra/home-information.git
synced 2026-04-18 05:29:14 -04:00
10 KiB
10 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'])
Related Documentation
- Testing guidelines: Testing Guidelines
- UI testing: UI Testing
- Backend testing: Backend Guidelines