Skip to content
UseQR
ESC

Jump to

MOVEOPEN50 places

Developers & agents

QR codes in CI and automated testing

Test QR codes by decoding them, not by eyeballing them: assert that a real decoder reads the rendered output back to the exact input. Snapshot the module matrix rather than PNG bytes, because rendering may legitimately differ across environments while the matrix must not. Run the verify step on every build so styling regressions fail CI.

View as MarkdownPaste this page into any AI assistant. It is plain, portable Markdown.

The only assertion that matters

If your product generates QR codes, your tests should prove they decode. Everything else (pixel comparisons, DOM assertions, "the image element rendered") is proxy evidence. The direct evidence is one line:

import { readBarcodes } from "zxing-wasm/reader";

const results = await readBarcodes(pngBytes, { formats: ["QRCode"], tryHarder: true });
expect(results[0]?.text).toBe(expectedPayload);

Note the equality check. Asserting results.length > 0 passes when a payload-building bug encodes the wrong data perfectly. The full reasoning is in the decode-verify loop; this page is about where the assertions live.

Snapshot the matrix, not the PNG

The tempting test (snapshot the generated PNG and diff bytes) is the wrong one. Rendering may legitimately differ across environments (font-free as QR rendering is, rasteriser versions, anti-aliasing and PNG encoder settings still vary) while the code remains perfectly valid. Byte-snapshots of pixels produce flaky tests that fail on a CI image upgrade and teach the team to ignore red builds.

The stable contract is one level down: the module matrix. It is pure output of data, encoding and mask choice, a boolean grid that must be bit-identical forever for the same inputs. Snapshot that:

const matrix = generateMatrix("https://example.com", { ecLevel: "M" });
expect(matrixToString(matrix)).toMatchSnapshot();   // rows of "X" and "."

Layered this way: matrix snapshots catch encoder changes; decode assertions catch rendering breakage; and if you promise byte-stable output (as a deterministic generator should), one dedicated test asserts same-input → byte-identical SVG: UseQR's core suite carries exactly that test, alongside round-trip decodes at every error-correction level.

Gates in the pipeline

Unit level: encode → render → rasterise → decode, in-process, per payload type. Crypto and payment payloads deserve their own cases: an EPC or UPI string that decodes but fails at the bank is a payload bug a decoder cannot see, so also assert on the payload structure itself.

Permutation level, if you offer styling, test the combinations, because styling is where scannability quietly dies. UseQR's CI decode-verifies the full matrix of module styles × eye styles × gradients × EC levels on every build; a styling regression is a failed build, not a customer's failed scan.

E2E level: test what the user actually gets. UseQR's Playwright suite fills the generator form, waits for the on-page "scan-verified" badge, downloads the SVG file, rasterises it in Node and decodes the downloaded artefact with zxing, proving the export path end to end, not just the preview.

No toolchain available? The keyless verify endpoint turns any CI step into a gate:

curl -sG "https://useqr.app/api/v1/verify" --data-urlencode "data=${URL}" \
  | jq -e '.scannable and .matchesInput' > /dev/null || exit 1

For generated fixtures (codes checked into the repo for scanner tests) decode them in CI too (programmatic decoding), so a designer's "small cleanup" of a fixture PNG cannot silently break it.

What CI cannot prove

A green pipeline proves the digital artwork decodes. Ink spread, laminate glare, and a phone at arm's length in bad light are physical problems, print a proof for anything going onto paper. CI's job is making sure the only remaining risks are physical.

FAQ

How do I test QR codes in CI?

Decode them with a real decoder and assert the result equals the input exactly. Run it per payload type at unit level, across styling permutations if you offer styling, and once end-to-end against the real download or API path.

Why not snapshot-test the QR PNG bytes?

Because rasterisation can differ across environments and library versions while the code stays valid, so pixel snapshots go red for non-bugs. Snapshot the module matrix instead. It must be bit-identical for identical inputs.

Can I verify QR codes in CI without installing a decoder?

Yes. GET https://useqr.app/api/v1/verify renders and decodes the code server-side and returns scannable and matchesInput in JSON, pipe through jq -e to fail the step. It is keyless, so there is no credential to manage in CI.

Does passing CI mean my printed QR code will scan?

It means the digital artwork decodes, which is necessary but not sufficient. Printing adds ink spread, glare and distance effects, so proof physical output at final size on final stock before a large run.

Try it: free, no signup

  • Building a decode-verify loop, The engineering pattern that proves a QR code scans: render, rasterise, decode with a real decoder, compare. How to build it, and the report fields that matter.
  • Why you should verify that a QR code decodes, Rendering a QR code proves nothing about whether it scans. Styling, colour, logos and print all consume error-correction budget invisibly. The only…
  • Bulk QR generation at scale, Generating hundreds to millions of QR codes: the batch API and its limits, local generation with worker pools, determinism as a caching strategy, and manifests.
  • QR code generation performance, What generating a QR code actually costs: encoding is microseconds, rendering dominates, and PNG rasterisation is the expensive step. Where optimisation pays.