Testing

Template code has a reputation for being hard to test, and it is mostly earned: you need a running app, a request, a session, and a browser-shaped tool to read the result. Partials sidestep all of that. render_partial is a function that takes a path and some keyword arguments and returns a string wrapped in an object. You can call it from a test the way you would call any other function, with no web framework in sight.

There is one thing to know first, and it is the reason this page exists.

The one-shot registration, and the fixture that tames it

register_extensions keeps its state in module globals, and with the default cache_init=True, a second call after a successful one is a no-op. In production that is a feature. In a test suite it is a trap: the first folder anyone registers wins for the rest of the session, and a test that registers a different folder will quietly keep rendering from the old one.

The fix is a fixture that registers the test folder and then clears the flag on the way out, so the next test starts clean:

from pathlib import Path

import pytest

import chameleon_partials


@pytest.fixture
def registered_extension():
    folder = (Path(__file__).parent / 'test_templates').as_posix()
    chameleon_partials.register_extensions(folder, auto_reload=True)
    yield
    chameleon_partials.has_registered_extensions = False

Resetting has_registered_extensions is all it takes. The next register_extensions call builds a fresh loader as if nothing had happened. Passing cache_init=False on every call works too, but the fixture keeps the knowledge in one place.

A registration that raised, say because the folder does not exist, never sets the flag in the first place. That means a failed call can simply be retried once the path is fixed, and it means a typo in a fixture path fails loudly rather than leaving a half-registered module behind.

Asserting on markup

With the fixture in place, a rendering test is three lines. Call the function, read html_text, look for the markup you expect:

def test_render_with_data(registered_extension):
    html = chameleon_partials.render_partial('render/with_data.pt', name='Sarah', age=32)
    assert '<span>Your name is Sarah and age is 32</span>' in html.html_text

Nested partials need nothing extra, because render_partial is injected into every partial’s model. A test that renders the video card has also exercised the thumbnail inside it, which is usually what you want: test the card people see, not the pieces in isolation.

Testing the promises about errors

The package makes two promises about errors, and tests can lean on both.

First, PartialsException means misuse and nothing else: rendering before registration, registering an empty or non-existent folder, or handing extend_model something that is not a dictionary. Second, everything Chameleon raises passes through untouched. A missing template is Chameleon’s ValueError, not a PartialsException, so code that already catches Chameleon’s errors keeps working when it starts rendering through partials:

def test_missing_template(registered_extension):
    with pytest.raises(ValueError, match='Template not found'):
        chameleon_partials.render_partial('no-way.pt')


def test_not_registered():
    with pytest.raises(chameleon_partials.PartialsException):
        chameleon_partials.render_partial('doesnt-matter.pt')

The second test is the one place the fixture must be absent. It has to run either before any fixture has registered a folder, or after one has reset the flag, which the fixture above guarantees.

Testing through the framework

Unit tests on fragments catch most things, but they will not catch a BeforeRender subscriber that config.scan() never found. For that, one functional test through the real app is worth a dozen mocks. The demo app does it with WebTest in example/tests/test_functional.py: fetch the home page, assert that the thumbnail markup is in the body. If that passes, the subscriber ran, the layout rendered, the page filled the slot, the loop iterated, and two levels of partials did their job. Nothing in the package needs mocking; register the real template folder in the app factory exactly as production does.

auto_reload in tests

auto_reload=True tells Chameleon to check a template’s modification time on each render and recompile when it changed. A test that writes template files on the fly needs it. A suite over a fixed set of template files does not, and the default False saves a stat call per render. The fixture above turns it on, which costs nothing noticeable and means a test can edit a template mid-run if it ever needs to.