Testing documents / Running it in CI
Testing document storage, retrieval and the expiry you configured
Exercising the store-and-retrieve path, including the part that only happens after time passes, which is the part nobody tests.
Why the obvious approach does not work
A pipeline that stores its output has a lifecycle: the document is created, retrieved some number of times, and then expires. The first two are exercised constantly and the third is exercised in production, months later, by a customer following an old link. Because expiry is time-based it cannot be reached by a normal test run, so the branch that handles a gone document is usually written from imagination and never executed. It is also the branch most likely to produce a confusing error rather than a clear one.
What to do instead
Each of these is a decision with a wrong answer, and the wrong answer usually produces a suite that passes rather than one that fails.
- Assert that storing returns an expiry and that your code records it. The render response carries document_expires when a document is stored, and a system that ignores it cannot tell a user when a link will stop working.
- Test the retrieval-after-expiry branch by stubbing the error rather than by waiting, and assert it produces a clear outcome rather than a generic failure.
- Assert your stored retention matches your policy. If the policy says thirty days and the request asks for seven, the documents are gone three weeks early and nobody finds out until somebody looks for one.
- Test that a document is retrievable immediately after storing, which catches a race between the store and the read.
- Test the signed-link path separately from the authenticated one, because they have different failure modes: a link has its own expiry, shorter than the document's, and a link that outlives its document is a different error from one that expired.
- Assert nothing sensitive is in the filename, since the filename is stored alongside the document and travels with it.
In practice
Test code, with the thing people write instead kept in a comment where it is the more instructive half.
test("storing a document returns an expiry we record", async () => {
const res = await client.render({
html, store: true, filename: "invoice-2026-118.pdf",
});
expect(res.document_id).toBeTruthy();
expect(res.document_expires).toBeTruthy();
const saved = await db.documents.find(res.document_id);
// Without this, nobody can tell a user when the link stops working.
expect(saved.expiresAt).toEqual(new Date(res.document_expires));
});
test("retention matches the policy", async () => {
const res = await client.render({ html, store: true });
const days = Math.round(
(new Date(res.document_expires) - Date.now()) / 86_400_000,
);
expect(days).toBe(RETENTION_DAYS); // the policy, not whatever was asked for
});
test("an expired document produces a clear outcome", async () => {
client.get.mockRejectedValue(new ApiError(410, "content_expired"));
const result = await fetchStored(docId);
expect(result).toEqual({ expired: true, message: "This document is no longer available." });
});
test("a signed link expires before the document does", async () => {
const link = await client.documentLink(docId, { ttl: 900 });
expect(new Date(link.expires_at) < new Date(doc.document_expires)).toBe(true);
});
test("the filename carries nothing sensitive", async () => {
const name = filenameFor(invoice);
expect(name).not.toMatch(/\b\d{6,}\b/); // no account or customer number
expect(name).not.toMatch(/@/); // no email address
});What people do instead
Never testing the expired branch because it cannot be reached without waiting. Stubbing the error takes a minute and covers the path that a real customer will hit, at a moment when nobody is watching and the resulting error message is the only thing they see.
What a failure actually tells you
A retention mismatch is the highest-value failure here, because it is silent in production: documents disappear earlier than the policy promises and the first symptom is somebody unable to find one.
Frequently asked
Why not just compare the PDF bytes?
Because two renders of identical input are not byte-identical. A PDF carries a creation timestamp and a document identifier, and font subsetting can differ between runs, so a byte comparison fails on the first run and keeps failing. The techniques here all pick a projection of the document that is stable across runs and still says something about whether it is correct.
How much of this is worth doing for one document?
The cheap end, and it is genuinely cheap: a page count and a handful of containment assertions on extracted text will catch most regressions for the cost of one render. Visual comparison and the full fixture matrix earn their keep when the document is customer-facing and numerous, and not before.
Does any of this need a browser in the CI image?
No. Rendering happens over an API call, so the runner needs a client rather than an engine, and the tests that need no document at all, the contract checks and the static pagination checks, need nothing. Keeping a browser in the image to run tests means testing a renderer that is not the one shipping.
Related testing techniques
The techniques that pair with this one, then the rest of the same group.
Testing what happens when a render fails, not just when it works
Exercising the failure branches of a document pipeline, which are the least-tested code in most systems and the code that runs on the worst day.
Keeping a document suite fast enough to stay in the pull request
The specific levers that reduce the cost of a rendering suite, which are different from the ones that speed up ordinary tests.
Testing the delivery of a document, not just its generation
Asserting on what actually leaves the system, since a correct document attached to the wrong message, or to the wrong recipient, is a worse failure than a malformed one.
Running document tests in CI without a browser in the image
Testing a rendering pipeline on a runner that has no renderer, which is the normal situation once rendering is a service rather than a dependency.
Catching pagination mistakes without rendering the document
Asserting on the rules that govern page behaviour rather than on the pages themselves, which is instant and catches the omissions rather than the outcomes.
Every document testing technique
The full list, grouped by what to assert, how to make runs comparable, and how to run it in CI.
What goes wrong in a rendered document
The failure modes these tests exist to catch, described from the symptom rather than the assertion.
Every technique here needs a document to assert on. Render one of your real templates first, then decide which projection of it is worth a test.