Zum Inhalt springen

Snapshot-Tests für Dokumente

Was man an einer Dokumentvorlage prüfen sollte, welche Datensätze einen Snapshot verdienen, wie der Diff aussieht, wenn die Buchhaltung die Fußzeile ändert — und die Lücke in unserem eigenen Werkzeug, die dieser Beitrag gefunden hat.

Eine kaputte Dokumentvorlage scheitert selten. Nichts wirft einen Fehler, die API antwortet mit 200, und ein PDF kommt heraus. Es ist nur ein anderes PDF als letzte Woche: Der Hinweis zur Steuerschuldnerschaft fehlt, in der Fußzeile steht noch die alte Bankverbindung, die Summe rechnet mit dem falschen Satz. Bemerkt wird das von denen, die es bekommen.

Tests für Dokumente müssen die Änderung finden, nicht den Absturz. Dieser Beitrag zeigt, wie wir das mit Snapshot-Tests machen und was wir beim Schreiben gelernt haben.

Was man vergleicht

Nicht das PDF. Seine Bytes ändern sich mit jedem Rendern — mindestens Erstellungs- und Änderungsdatum in den Metadaten —, also scheitert ein Byte-Vergleich ohne Grund, und ein gescheiterter Vergleich zweier PDFs sagt niemandem, was sich geändert hat.

Vergleichen lohnt sich das Dokument, bevor es gedruckt wird: das HTML, das die Engine aus der Vorlage und einem Datensatz zusammensetzt. Es ist deterministisch, es ist Text, und ein Unterschied darin ist ein Unterschied, den ein Mensch lesen kann. Genau das macht formfeed test: Es rendert jeden Datensatz in templates/<slug>/data/*.json offline, mit derselben Engine wie die Render-Server, und vergleicht das Ergebnis mit dem genehmigten Snapshot in templates/<slug>/tests/__snapshots__/<Datensatz>.html. formfeed test -u genehmigt den aktuellen Stand. Eines hebt den Determinismus auf: Ein Datum von der Uhr — date('now') oder date auf einem Feld, das der Datensatz leer lässt — ändert sich jeden Tag; geben Sie jedem Datensatz deshalb seine Datumswerte mit.

Welche Datensätze einen verdienen

Ein Snapshot sieht nur, was seine Daten auslösen. Die Rechnung in unserem Beispielprojekt enthält eine Bedingung:

{% if invoice.reverse_charge %}<p class="note">Steuerschuldnerschaft des Leistungsempfängers.</p>{% endif %}

Gibt es nur die gewöhnliche Rechnung als Datensatz, wird diese Zeile nie gerendert, und nichts prüft sie. Das Projekt hat deshalb zwei Datensätze, default und reverse-charge. Ändert sich der Wortlaut des Hinweises, meldet formfeed test:

--- invoice/reverse-charge (approved)
+++ invoice/reverse-charge (current)
@@
  </table>
  <p class="total">Gesamt 960,00 €</p>
- <p class="note">Steuerschuldnerschaft des Leistungsempfängers.</p>
+ <p class="note">Steuerschuldnerschaft des Leistungsempfängers (§ 13b UStG).</p>

  </body>
1 passed, 1 failed, 0 without snapshot, 0 written

Die gewöhnliche Rechnung besteht; der eine Datensatz, der den Hinweis zeigt, scheitert. Daraus folgt eine einfache Regel: ein Datensatz je Verzweigung der Vorlage. Die Rechnung mit einer Position und die mit vierzig, der Kunde ohne USt-IdNr., die Gutschrift, die Sprache, in die Sie übersetzen. Der Editor nutzt dieselben Datensätze für seine Vorschau, sie sind also selten Zusatzarbeit.

Wenn die Buchhaltung die Fußzeile ändert

Jetzt eine Änderung, die jedes Unternehmen früher oder später macht: die Bankverbindung in der Fußzeile. Die Buchhaltung ergänzt die BIC.

Die Fußzeile eines PDFs ist ein Sonderfall. Chromium druckt sie aus einem eigenen Template, im Seitenrand, außerhalb des Dokuments — und als wir das für diesen Beitrag ausprobiert haben, bestand der Test. formfeed test verglich nur den Dokumentinhalt, und eine geänderte footer.html ging unbemerkt durch. Also haben wir es behoben: Seit formfeed 0.3.7 enthält der Snapshot auch Kopf- und Fußzeile, jeweils unter einer Markierungszeile, und dieselbe Änderung liest sich so:

--- invoice/default (approved)
+++ invoice/default (current)
@@
  <!-- formfeed:footer -->
  <div style="width:100%;padding:0 18mm;font-size:8px;color:#555;display:flex;justify-content:space-between">
-   <span>Fennlor Studio GmbH · IBAN DE36 0000 0000 0000 0000 00</span>
+   <span>Fennlor Studio GmbH · IBAN DE36 0000 0000 0000 0000 00 · BIC XXXXDEXXXXX</span>
    <span>Seite <span class="pageNumber"></span> von <span class="totalPages"></span></span>
  </div>

— einmal für jeden Datensatz, denn jede Rechnung trägt die Fußzeile. Haben Ihre Vorlagen eine Kopf- oder Fußzeile und kommen Sie von formfeed 0.3.6 oder älter, scheitert das erste formfeed test nach dem Update genau aus diesem Grund einmal; führen Sie formfeed test -u aus und committen Sie die Snapshots.

Im Pull Request

Wer die Vorlage ändert, führt formfeed test -u aus und committet die neuen Snapshots mit. Der Pull Request trägt die Änderung dann zweimal: in footer.html und in jedem Snapshot, den sie berührt —

templates/invoice/footer.html                             | 2 +-
templates/invoice/tests/__snapshots__/default.html        | 2 +-
templates/invoice/tests/__snapshots__/reverse-charge.html | 2 +-

—, sodass ein Reviewer nicht nur sieht, was bearbeitet wurde, sondern welche Dokumente es erreicht. Ein Snapshot, der sich ändert, obwohl das niemand wollte, ist der Befund.

HTML zu lesen ist allerdings nicht dasselbe, wie die Rechnung anzusehen, also rendert die Pipeline sie auch. formfeed ci preview rendert die seit dem Basis-Branch geänderten Vorlagen mit einem Test-Schlüssel über die API, speichert die PDFs und schreibt eine Zusammenfassung für den Pull Request. Aus dem Lauf für diesen Beitrag, gegen unseren lokalen Stack:

## Formfeed preview

Templates changed against `main`: 1

| Template | Data set | Pages | Output |
| --- | --- | --- | --- |
| `invoice` | default | 1 | [invoice.pdf](…signierter Link…) |

Rendered with a Formfeed test key; links expire.

Die GitHub Action in der CI-Dokumentation führt bei jedem Pull Request formfeed validate und formfeed test aus, mit einem Test-Schlüssel als api-key auch formfeed ci preview, und postet die Zusammenfassung als einen Kommentar, den jeder Lauf aktualisiert. Test-Schlüssel werden nicht gezählt, die Vorschau kostet also nichts.

Was ein Snapshot nicht sieht

Ein Snapshot vergleicht Markup, kein Layout. Eine Zeile, die auf Seite zwei rutscht, weil eine Zeile darüber länger geworden ist, ändert das HTML überhaupt nicht. Dafür gibt es die gerenderte Vorschau im Pull Request, die paginierte Vorschau im Editor und Echtes Rendering — und für den Rest der Testeinrichtung den Leitfaden zum Testen. Warum Vorlagen überhaupt ins Repository gehören, steht in einem früheren Beitrag.

Ausgeführt am 21. September 2026 mit der formfeed-CLI an einem Beispielprojekt; die Ausgaben oben sind aus diesem Lauf kopiert.

Philipp Staudt

Heute mit dem Rendern beginnen

Der kostenlose Plan enthält 100 Einheiten im Monat und kostenlose Testrenderings. Wechseln Sie den Plan, wenn Sie mehr brauchen.