Testing templates with pytest

Check every template compiles, and test rendered output.

Every template compiles

A typo in a TAL attribute or an expression is a compile error. Chameleon compiles a template when it’s first rendered, so a broken template can go unnoticed until someone opens that page. This test compiles all of them:

# tests/test_templates_compile.py
from pathlib import Path

import pytest

from chameleon_rs import PageTemplateFile

TEMPLATES = Path(__file__).parent.parent / 'myapp' / 'templates'


@pytest.mark.parametrize('path', sorted(TEMPLATES.rglob('*.pt')), ids=str)
def test_template_compiles(path):
    PageTemplateFile(str(path)).cook_check()

cook_check() compiles without rendering, so the test needs no model. chameleon-rs compiles in about 0.05 ms per template, so even a few hundred templates take well under a second.

The CHAMELEON_EAGER=1 environment variable does the same at load time: file templates then compile as soon as they’re created.

Test the rendered output

Render a partial with the model your view would pass, and assert on the HTML:

import chameleon_rs


@pytest.fixture(autouse=True)
def templates():
    chameleon_rs.configure(template_folder=str(TEMPLATES))
    yield
    chameleon_rs.partials.clear()


def test_video_card_shows_title():
    html = chameleon_rs.render_partial('shared/video_card.pt', video=Video(1, 'Intro')).html_text
    assert '<a href="/videos/1">Intro</a>' in html

The fixture’s clear() forgets the configuration, so tests that configure a different folder don’t leak into each other.

For a Flask app, use its test client. See HTMX fragments for an example.

Compare against Chameleon

While you’re migrating, a test can render the same template with both engines and compare. Install stock Chameleon in the test environment only:

import chameleon
import chameleon_rs


def test_same_output_as_chameleon():
    path = str(TEMPLATES / 'home' / 'index.pt')
    model = {'title': 'Home', 'videos': []}
    assert chameleon_rs.PageTemplateFile(path)(**model) == chameleon.PageTemplateFile(path)(**model)

On a running staging app, shadow mode does this for every render.