# 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.

Source: https://useqr.app/docs/developers/qr-codes-in-ci-and-automated-testing · Last reviewed 2026-08-21 · UseQR is free forever, no signup.

---

## 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:

```js
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](/docs/developers/building-a-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:

```js
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](/docs/spec/deterministic-qr-generation) 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](/docs/payments/epc-qr-code-format) or [UPI](/docs/payments/upi-qr-code-format)
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:

```bash
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](/docs/developers/decode-a-qr-code-programmatically)),
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](/docs/how-to/how-to-test-a-qr-code-before-printing) 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

- https://useqr.app/validate
- https://useqr.app/scan
- https://useqr.app/bulk
