Skip to main content

Testing partials

Partials are small and explicit, so test them small: rendering fragments directly in Flask, FastAPI, Starlette, Quart, and plain Jinja2 environments.

The designer’s refresh from Why partials has landed. The view count has thousands separators, the author is a link, and because there is one card instead of fourteen, the change was one file. The remaining question is how you know it is right on every page, and the pleasant answer is that you do not have to look at every page. A partial has an explicit list of inputs and produces a string, which is about the most testable shape a piece of a web app can have. No browser, no full page, no database; you need the fragment’s name and a few keyword arguments. The patterns below are the ones the library’s own suite uses, so they are known to work.

Flask

render_partial with no renderer falls back to flask.render_template, which needs an app and a request context. A fixture provides both, and every test after that is two lines:

import flask
import pytest
import jinja_partials


@pytest.fixture
def app():
    app = flask.Flask(__name__, template_folder='templates')
    jinja_partials.register_extensions(app)
    with app.test_request_context('/'):
        yield app


def test_video_square(app):
    html = jinja_partials.render_partial('shared/partials/video_square.html', video=FakeVideo())
    assert '12,500 views' in html

That assertion is the thousands separators from the refresh, checked once, on behalf of every page that renders the card. A bad template name raises jinja2.TemplateNotFound, so pytest.raises(TemplateNotFound) is the right assertion when you want to prove a rename did not leave a dangling reference behind.

FastAPI and Starlette

After registration, the bound function is the render_partial global on the environment. You can call it directly, and you do not need the app running:

from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
import jinja_partials

templates = Jinja2Templates(directory='templates')
app = FastAPI()
jinja_partials.register_fastapi_extensions(app, templates)


def test_card_partial():
    render = templates.env.globals['render_partial']
    html = render('shared/partials/card.html', item={'title': 'Python Basics'})
    assert 'Python Basics' in html

Outside a lifespan, async environments render through a per-registration fallback pool, so a plain synchronous test function is enough. When you do want the full request path, with TestClient(app) as client: runs the lifespan; the pool is created on entry and shut down on exit, and the app can be started and stopped as many times as the suite likes. That re-entrancy is tested, because it was once broken.

Quart

Quart’s app.test_app() context runs the before_serving and after_serving hooks, which is where the render pool lives. Drive it with asyncio.run from an ordinary synchronous test:

import asyncio
from quart import Quart
import jinja_partials


def test_quart_partial():
    app = Quart(__name__, template_folder='templates')
    jinja_partials.register_quart_extensions(app)

    async def cycle():
        async with app.test_app():
            return app.jinja_env.globals['render_partial']('shared/partials/badge.html', tag='beginner')

    assert 'beginner' in asyncio.run(cycle())

Plain Jinja2

from jinja2 import Environment, FileSystemLoader
import jinja_partials

env = Environment(loader=FileSystemLoader('templates'), autoescape=True)
jinja_partials.register_environment(env, markup=True)


def test_fragment():
    html = env.get_template('home/index.html').render(videos=[FakeVideo()])
    assert '<img src=' in html

If a test is specifically about escaping, assert isinstance(result, Markup). render_partial returns markupsafe.Markup whenever markup=True, and a plain str when it is not, which is the difference between a rendered card and a page full of angle brackets.

One file, one test, fourteen pages covered. A partial that passes in isolation renders the same way everywhere, because it never borrows anything from the page that calls it, and How it works shows how little machinery that guarantee rests on.