Testing

Every template ships a fixture and a snapshot. The golden tests catch the two regressions nothing else does: a Tailwind class silently purged at build, and a ${} placeholder that stopped resolving after a refactor.

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.

The golden test base class

imio.emailkit.golden.GoldenTemplateTests generates one test per 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.

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.