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.

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 no tal: / 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-emails too.

Regenerating snapshots

make update-golden

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:

  1. 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.
  2. Authoring lint. The eight rules, over the .vue sources.

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-emails for 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 AttachmentError case.
  • 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:domain makes every i18n:translate render its msgid's English default, which is indistinguishable from success.