Testing
Every template ships a fixture. A chosen few also ship a snapshot — the golden gate is a smoke test of the rendering pipeline, not a byte-comparison of everything you render.
make test # the whole suite
make test-coverage # with coverage
make update-golden # regenerate the snapshots, deliberately
make check-emails # the lint and staleness gates CI runs
The layout
tests/
├── fixtures/
│ └── item_published.py # a CONTEXT dict
└── golden/
├── item_published.fr.html
├── item_published.fr.txt
├── item_published.en.html
└── item_published.en.txt
A fixture is a module exporting CONTEXT. A snapshot is the rendered output, per template, per language, per part.
How many templates to snapshot
Fewer than you have. Set templates to a subset:
class TestGoldenTemplates(GoldenTemplateTests):
templates = ("item_published",) # not every template you register
languages = ("fr", "en")
A byte comparison of a whole rendered mail fails for two very different reasons,
and it cannot tell them apart. A purged Tailwind class or a ${} that stopped
resolving is one — a real regression, and the reason the gate exists. Any edit to
a shared layout, or a Plone point release that reflows the markup, is the other:
it fails every snapshot at once, and a wall of diffs that all have to be
regenerated is a wall nobody reads. imio.emailkit snapshots one of its four
templates for exactly this reason.
What you lose is less than it looks. The placeholder audit
(test_golden_has_no_unresolved_placeholder) is the check that catches the
regression you care about, and make check-emails compares your committed build
against a fresh one byte for byte regardless of how many snapshots you keep.
Two rules the gate does still enforce, whatever subset you pick: a snapshot set must be complete (every language, both parts — a half-generated set silently checks less than it looks like it does), and no snapshot may outlive the template it was made from.
The golden test base class
imio.emailkit.golden.GoldenTemplateTests generates one test per snapshotted template × language × part. Subclass it in your add-on:
from imio.emailkit.golden import GoldenTemplateTests
class TestGoldenTemplates(GoldenTemplateTests):
"""Render every template this add-on registers, diff against the snapshots."""
It asserts three things:
- Name
test_matches_golden- Description
The render matches the committed snapshot, with a truncated unified diff on failure — labelled so the two sides cannot be confused.
- Name
test_golden_has_no_unresolved_placeholder- Description
No
${...}survived into the output, and notal:/i18n:/metal:attribute was left behind.
- Name
test_every_template_has_a_fixture- Description
A registered template with no fixture is a failure, not a skip — every template, not just the snapshotted ones. A fixture is read by the preview and by
bin/preview-emailstoo.
Regenerating snapshots
make update-golden
Snapshots are regenerated only by this target, never as a side effect of a failing comparison. A snapshot that repairs itself when it breaks is not a snapshot.
Two rules for anyone adding a test here
Assert on substituted values, never on marker strings. Without the IPageTemplateEngine utility, zope.pagetemplate falls back to zope.tal, where ${...} passes through verbatim and raises nothing — so a test that only checks "our marker is present" stays green while raw placeholders ship.
Assert on rendered output, never only that a file was resolved. A z3c.jbot override can be correctly mapped and still not take effect; see the meta.zcml warning on Overrides & theming.
The staleness gate
make check-emails runs two gates in one script:
- Staleness. Compile each package into a temporary directory and diff against the committed
templates/. Exit 1 with a per-file diff summary if they disagree. - Authoring lint. The eight rules, over the
.vuesources.
This is the CI gate. Every add-on that ships templates — including imio.emailkit itself — runs it in its pipeline. It is the price of committing the build output, and it is cheaper than making Node a production dependency.
The plaintext twin
A <name>.txt.pt twin is hand-authored where plaintext quality matters. Maizzle's plaintext output destroys tal: and i18n: constructs, so generating it would ship a plausible-looking body with the wrong content in the wrong language.
Without a twin, render() falls back to a naive text extraction and logs a deprecation. The mail still sends; it just is not as good.
What imio.emailkit tests about itself
Worth knowing, because these double as living documentation of the contracts:
- Discovery, with two dummy add-ons.
- Golden files and
check-emailsfor its own templates — dogfooding the full consumer contract. - Recipient resolution: strings, members, userids, mixed iterables, and every failure.
- Attachment sources: bytes, path, file object, blob value, content object; metadata inference; every
AttachmentErrorcase. - Language grouping for
.send(). - A transaction-abort test:
.send()then abort, and the MailHost queue is empty. - That FR and NL output actually differ — because a missing
i18n:domainmakes everyi18n:translaterender its msgid's English default, which is indistinguishable from success.
Related
- Translations — why the FR/NL assertion above exists.
- Preview & send-test — the same fixtures, rendered in a browser.
- Source:
imio/emailkit/golden.py