Testing partials
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 htmlThat 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 htmlOutside 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 htmlIf 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.