Jasmin Börsig

Jasmin Börsig

Software Architect / persolog GmbH

Diffing PDFs in production: how a solo developer built a regression test harness her product team actually uses

A story of two architectures for visual regression testing PDF output — and the hard-earned lessons that survived the migration.

When the output of your application is a print-ready PDF, the usual testing strategies fall apart. You can’t snapshot a DOM, you can’t assert on JSON, and “does this look right?” is a question only a human eye can answer. At persolog, every personality profile we ship is a multi-page PDF rendered through an InDesign-server pipeline — and as the solo developer with full product ownership, I needed a way for non-technical product owners to verify their template changes without me sitting next to them.

The strategic move is to stop curating fixture files and treat production itself as the reference oracle: the same questionnaire answers are run through staging and production, and the resulting PDFs are diffed page by page with de.redsix:pdfcompare. This talk is the story of two architectures built around that idea — and what changed between them.

Version one was a JUnit-and-CI harness: tests in Kotlin, parallelism via maxParallelForks, polling via Awaitility, runBlocking bridging suspend Ktor calls into JUnit, and a GitLab pipeline product owners triggered by filling in CI variables. It worked — it has been running in CI for over a year — but it had three different concurrency models stacked on top of each other and a UX that asked non-developers to read CI variable definitions.

Version two is a single Kotlin service. Ktor server, structured concurrency as the only concurrency model, kotlinx.html for a UI built for the product owners, Keycloak for auth, files on disk for the artifacts. Same testing strategy, none of the CI dependency, and a much cleaner answer to “what is the tool?”.

We’ll walk through both architectures, the migration between them, and the hard-earned lessons that survived: font substitution, timeout strategy, false-positive diffs, and when “production as oracle” stops being a clever trick and starts being a liability.

What you’ll take away:

  • A concrete pattern for visual regression testing non-web outputs (PDFs, generated images, print artifacts) from Kotlin
  • Production as oracle: when running prod and staging side by side is the right test strategy, and when it isn’t
  • Structured concurrency as a unifying model — replacing maxParallelForks + Awaitility + runBlocking with one coroutine story
  • Ktor server + kotlinx.html as a low-ceremony stack for internal tools you actually want your team to use
  • Keycloak / OIDC auth on a Ktor service — the minimum viable integration
  • Designing for non-developers without scope creep — the discipline that keeps a small tool small
  • The unglamorous failure modes: font substitution, server load, flaky timeouts, false-positive diffs

Jasmin Börsig (she/her) is a Software Architect at persolog GmbH in Germany. Since 2021 she has led the rebuild of persolog Online — a Kotlin/Spring Boot microservice platform with a Vue 3 frontend, serving an international network of trainers and companies in 10+ languages. She started the project as the sole developer and grew it into a self-organized team while owning architecture, product decisions, and large parts of the implementation.

Before persolog she spent eight years at abas Software GmbH building Gradle plugins, SDKs, and developer-training infrastructure across Kotlin, Java, and Scala. She is iSAQB-certified (Foundation Level) and is currently studying philosophy on the side at FernUniversität Hagen.