Testing¶
Shrd works well with pytest and pytest-django. You do not need a browser, Selenium, or a JavaScript test runner — components render server-side and actions are plain HTTP requests.
This guide covers testing your Django project that uses Shrd. For running the Shrd framework's own test suite, see Framework development at the bottom.
Prerequisites¶
Before writing tests, confirm your project has the same pieces Shrd needs at runtime:
| Requirement | Why |
|---|---|
"shard" in INSTALLED_APPS |
Registers the app and autodiscovers components |
path("shard/", include("shard.urls")) |
HTMX action and render endpoints |
CACHES configured |
Component state (props, state, slots) is stored in Django's cache |
DJANGO_SETTINGS_MODULE for pytest |
pytest-django needs your settings module |
Example cache for tests (in-memory is fine):
# settings.py or settings/test.py
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
}
}
If your URL namespace is not "shard", set SHARD_URL_NAMESPACE to match your include() name and use that prefix in test URLs.
Install test dependencies¶
In your project:
You do not need Shrd's development extras unless you are contributing to the framework itself.
pytest configuration¶
Add to pyproject.toml:
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "myproject.settings"
pythonpath = ["."]
testpaths = ["tests"]
Or use pytest.ini:
Recommended conftest.py¶
Create tests/conftest.py in your project:
import pytest
from shard.testing import extract_instance_id
@pytest.fixture
def shard_instance_id():
return extract_instance_id
@pytest.fixture(autouse=True)
def clear_shard_cache():
"""Optional: reset cache between tests if you reuse instance IDs."""
from django.core.cache import cache
cache.clear()
yield
cache.clear()
Component registration¶
With SHARD_AUTODISCOVER = True (the default), components in <app>.components are registered automatically — no extra fixture needed.
If autodiscover is disabled, register components in conftest.py:
import pytest
from shard.registry import register
from myapp.components import Counter, Card
@pytest.fixture(autouse=True)
def register_components():
for cls in (Counter, Card):
register(cls)
yield
Three ways to test¶
1. Render HTML with mount() or mount_component()¶
Fastest way to test templates, props, slots, scoped CSS, and computed values. No HTTP required.
from shard import mount, mount_component
from myapp.components import Counter, Card
def test_counter_renders_initial_count():
html = mount(Counter, props={"initial": 5, "step": 1})
assert "Current count is 5" in html
assert 'data-shard-scope="counter"' in html
def test_mount_component_returns_instance_metadata():
result = mount_component(Counter, props={"initial": 3, "step": 1})
assert result.instance_id
assert result.component.state["count"] == 3
assert "Current count is 3" in result.html
def test_card_renders_slot_content():
html = mount(
Card,
props={"title": "Hello"},
slots={"default": "<p>World</p>"},
)
assert "Hello" in html
assert "<p>World</p>" in html
mount() creates a new instance (random ID), persists state to cache, and returns rendered HTML — the same path production uses on first render.
Use render_component() when you need more control (e.g. persist=False for pure snapshot tests).
2. Test action logic directly¶
Call dispatch_action() on a component instance to test server logic without routing or HTMX headers.
import pytest
from shard.exceptions import ActionNotFoundError
from myapp.components import Counter
def test_increment_updates_state():
component = Counter(props={"initial": 0, "step": 2})
new_state = component.dispatch_action("increment", {})
assert new_state["count"] == 2
def test_unknown_action_raises():
component = Counter(props={"initial": 0, "step": 1})
with pytest.raises(ActionNotFoundError):
component.dispatch_action("not_real", {})
Also useful for lifecycle hooks (before_action, after_action), @computed methods, and ActionResult with events or redirects.
3. Test HTMX endpoints with Django's test client¶
Integration tests that exercise the full request cycle: URL routing, cache load/save, response headers, and HTML fragments.
import json
import pytest
from django.test import Client
from shard import mount_component
from shard.testing import post_action
from myapp.components import Counter
@pytest.fixture
def client():
return Client()
def test_increment_action_via_htmx(client, shard_instance_id):
result = mount_component(Counter, props={"initial": 1, "step": 2})
response = post_action(client, result.instance_id, "increment")
assert response.status_code == 200
assert "Current count is 3" in response.content.decode()
events = json.loads(response["HX-Trigger"])
assert events["counter:changed"] is True
assert events["shard:action-complete"] is True
post_action() uses your SHARD_URL_NAMESPACE and sets HTTP_HX_REQUEST automatically.
Manual equivalent:
def test_increment_action_via_htmx(client, shard_instance_id):
result = mount_component(Counter, props={"initial": 1, "step": 2})
response = client.post(
f"/shard/action/{result.instance_id}/increment/",
HTTP_HX_REQUEST="true",
)
Important details:
- Set
HTTP_HX_REQUEST="true"— Shrd rejects non-HTMX action requests with 400 - Mount the component first so state exists in cache before posting to the action URL
- Parse
HX-Triggerto assert@emitsevents
Test the render endpoint the same way:
def test_render_endpoint_returns_fresh_html(client, shard_instance_id):
html = mount(Counter, props={"initial": 7, "step": 1})
instance_id = shard_instance_id(html)
response = client.get(f"/shard/render/{instance_id}/")
assert response.status_code == 200
assert "Current count is 7" in response.content.decode()
Testing pages and template tags¶
Full pages that use {% component %} can be tested with the Django client:
def test_home_page_renders_components(client):
response = client.get("/")
assert response.status_code == 200
assert "Primary action" in response.content.decode()
Template tags ({% shard_scripts %}, {% shard_htmx %}, etc.) can be tested with Django's template engine:
from django.template import Context, Template
def test_shard_scripts_includes_htmx():
template = Template("{% load shard %}{% shard_scripts %}")
html = template.render(Context({}))
assert "htmx.min.js" in html
Testing prop validation¶
Invalid props raise PropValidationError at instantiation:
import pytest
from shard.exceptions import PropValidationError
from myapp.components import Counter
def test_invalid_prop_type_raises():
with pytest.raises(PropValidationError):
Counter(props={"initial": "not-a-number", "step": 1})
What you usually do not need¶
| Often unnecessary | Why |
|---|---|
@pytest.mark.django_db |
Shrd state lives in cache, not the database |
| Browser / Playwright | No client-side framework to hydrate |
| Mocking HTMX | Post to Django URLs with HTTP_HX_REQUEST |
| Separate frontend build | Templates render server-side in tests |
Add django_db only when your components or views touch the ORM.
Tips¶
- Use
shard.testing—extract_instance_id()andpost_action()avoid brittle HTML parsing - Prefer
mount_component()when you need the instance ID without parsing HTML - Slots survive actions — cache stores slot HTML; re-render endpoints should still include slot content
- HTMX partials omit scoped CSS — assert
"<style data-shard-styles="is absent on action responses if you test partial swaps - Isolate tests — each
mount()gets a new UUID; usecache.clear()if you construct components with fixedinstance_id - List registered components —
python manage.py shard_listhelps verify autodiscover during setup - Run
shard_doctor— checks cache, URL namespace, and missing component templates
Example test layout¶
myproject/
├── myapp/
│ └── components.py
└── tests/
├── conftest.py
├── test_components.py # mount() and dispatch_action()
├── test_htmx_actions.py # client.post to /shard/action/...
└── test_pages.py # full page responses
Framework development¶
This section is for contributors working on the Shrd package itself.
Run the framework test suite¶
With coverage¶
Current coverage: ~93% of the shard package (≥90% enforced in CI).
Test layout¶
| File | What it covers |
|---|---|
test_props.py |
Prop validation, coercion, descriptors |
test_scoping.py |
CSS scoping engine |
test_state.py |
Cache persistence for props/state/slots |
test_component.py |
Actions, computed, hooks, events, nesting |
test_registry.py |
Component registration |
test_htmx.py |
HTMX and Alpine attribute builders |
test_templatetags.py |
Django template tags |
test_views.py |
HTTP endpoints and error responses |
test_styles.py |
Scoped stylesheet loading |
test_integration.py |
Example app end-to-end flows |
Framework tests use tests/settings.py, which extends the example project settings and adds tests/templates/ to TEMPLATES["DIRS"]. Isolated test components live in tests/support/components.py and are registered via tests/conftest.py.
CI¶
The ci.yml workflow runs pre-commit, Ruff, pytest (Python 3.10–3.12, ≥90% coverage), size budgets, and docs build on every pull request.