djust docs
Browse documentation

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:

  1. Mounts each view
  2. Asserts it renders without raising an exception
  3. Checks DB query counts stay under max_queries
  4. 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 in myapp/tests/
  • One test file per view or module: test_counter_view.py
  • Use @pytest.mark.django_db for tests that need database access
  • Use TestCase subclasses 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_settings or assertNumQueries