Testing
On this page
djust provides a testing toolkit that lets you test LiveViews without a browser or WebSocket connection.
Running Tests
make test # All tests (Python + Rust + JavaScript)
make test-python # Python only
make test-rust # Rust only
pytest python/ # All Python tests
pytest python/djust/tests/test_foo.py # Single file
pytest -k "test_increment" # By name pattern
pytest -x # Stop on first failure
pytest --lf # Re-run last failures
LiveViewTestClient
The primary tool for unit-testing LiveViews. Send events and assert state changes without a server:
from django.test import TestCase
from djust.testing import LiveViewTestClient
from myapp.views import CounterView
class TestCounterView(TestCase):
def test_increment(self):
client = LiveViewTestClient(CounterView)
client.mount()
client.send_event("increment")
client.assert_state(count=1)
client.send_event("increment")
client.assert_state(count=2)
def test_decrement_does_not_go_negative(self):
client = LiveViewTestClient(CounterView)
client.mount()
client.send_event("decrement")
client.assert_state(count=0) # Assuming count is floored at 0
Testing with Authentication
from django.contrib.auth.models import User
class TestProtectedView(TestCase):
def setUp(self):
self.user = User.objects.create_user("testuser", password="pass")
def test_requires_login(self):
# LiveViewTestClient.mount() does not run auth checks, so test the
# login gate through Django's test Client on the view's URL.
response = self.client.get("/protected/")
self.assertEqual(response.status_code, 302)
self.assertIn("/login/", response["Location"])
def test_authenticated(self):
client = LiveViewTestClient(MyProtectedView, user=self.user)
client.mount()
self.assertEqual(client.view_instance.request.user, self.user)
LiveViewTestClient.mount() calls the view's mount() directly, without the
login_required / permission check, and user is not part of the view's
state. Assert on state your view sets from request.user instead.
Testing with URL Parameters
def test_item_detail(self):
item = Item.objects.create(name="Test Item")
client = LiveViewTestClient(ItemDetailView)
client.mount(item_id=item.pk)
client.assert_state(item=item)
Asserting Rendered HTML
def test_renders_item_list(self):
client = LiveViewTestClient(ItemListView)
client.mount()
client.send_event("search", value="laptop")
html = client.render()
self.assertIn("laptop", html)
self.assertNotIn("phone", html)
Sending Events with Data
def test_delete_item(self):
client = LiveViewTestClient(ItemListView)
client.mount()
# Simulate dj-click="delete" data-item-id="5"
client.send_event("delete_item", item_id=5)
client.assert_state(items=[]) # or whatever the expected state is
SnapshotTestMixin
Compare rendered HTML against stored snapshots. Useful for regression testing UI changes:
from django.test import TestCase
from djust.testing import LiveViewTestClient, SnapshotTestMixin
from myapp.views import ProductCardView
class TestProductCard(TestCase, SnapshotTestMixin):
def test_renders_product(self):
client = LiveViewTestClient(ProductCardView)
client.mount(product_id=1)
self.assert_html_snapshot("product_card_1", client.render())
On first run, the snapshot is saved to snapshots/product_card_1.html.snapshot next to the test file. Subsequent runs compare against it. To update snapshots after an intentional change:
UPDATE_SNAPSHOTS=1 pytest
You can also set update_snapshots = True on the test class, and change the directory with snapshot_dir (default "snapshots").
LiveViewSmokeTest
Auto-discover all LiveViews in an app and run basic smoke tests — verifies they mount without crashing and don't hit excessive queries:
from django.test import TestCase
from djust.testing import LiveViewSmokeTest
class TestAllViews(TestCase, LiveViewSmokeTest):
app_label = "myapp" # Only test views in this app
max_queries = 20 # Fail if mount + render exceeds this many DB queries (default 50)
fuzz = True # Send XSS/type-mismatch payloads to handlers (default True)
LiveViewSmokeTest auto-discovers all LiveView subclasses in app_label and:
- Mounts each view
- Asserts it renders without raising an exception
- Checks DB query counts stay under
max_queries - If
fuzz=True, sends malformed event payloads to handlers and asserts no 500 errors
Performance Testing
Ensure handlers meet response-time and query-count budgets:
from djust.testing import LiveViewTestClient, performance_test
from django.test import TestCase
class TestSearchPerformance(TestCase):
@performance_test(max_time_ms=50, max_queries=3)
def test_search_is_fast(self):
client = LiveViewTestClient(SearchView)
client.mount()
client.send_event("search", value="test")
The test fails if the whole test method (including client setup and mount()) takes more than 50 ms or runs more than 3 queries. @performance_test also accepts track_memory=True and max_memory_bytes.
Testing Forms
from django.test import TestCase
from djust.testing import LiveViewTestClient
from myapp.views import ContactFormView
class TestContactForm(TestCase):
def test_valid_submission(self):
client = LiveViewTestClient(ContactFormView)
client.mount()
client.send_event("submit_form", name="Alice", email="alice@example.com", message="Hi")
client.assert_state(is_valid=True)
self.assertTrue(client.get_state()["success_message"])
def test_invalid_email(self):
client = LiveViewTestClient(ContactFormView)
client.mount()
client.send_event("submit_form", name="Bob", email="not-an-email", message="Hi")
client.assert_state(is_valid=False)
# Field errors should be set
state = client.get_state()
self.assertIn("email", state["field_errors"])
def test_realtime_validation(self):
client = LiveViewTestClient(ContactFormView)
client.mount()
# Simulate dj-change="validate_field" on email field
client.send_event("validate_field", field_name="email", value="invalid")
state = client.get_state()
self.assertIn("email", state["field_errors"])
JavaScript Tests
JavaScript tests live in tests/js/ and run with Node.js:
make test-js
# or, from the repository root
npm test
Each JS feature file in static/djust/src/ should have a corresponding test in tests/js/. See JavaScript Testing for details.
pytest Configuration
Recommended pyproject.toml settings:
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "myproject.settings"
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"slow: marks tests as slow (deselect with '-m not slow')",
"asyncio: marks async tests",
]
Test File Conventions
- Tests live in
tests/at the project root or inmyapp/tests/ - One test file per view or module:
test_counter_view.py - Use
@pytest.mark.django_dbfor tests that need database access - Use
TestCasesubclasses for tests that need transactions
Best Practices
- Test
mount()separately from event handlers - Test one event per test method for clarity
- Use
client.assert_state()to check specific state without inspecting all of it - Use
client.render()only when you need to assert HTML content - For DB-heavy views, check query counts with
django.test.utils.override_settingsorassertNumQueries