Snapshot testing
PdfPecker.Testing checks in your unit tests that a PDF still comes out as it did. PdfAssert.MatchesSnapshot
compares the PDF your code makes with a stored one, leaving out what changes on every run (creation dates, file IDs,
the compressed bytes of streams), and when they differ it writes page images with the changed pixels marked. It also
checks for text and page counts.
dotnet add package PdfPecker.Testing
It works with xUnit, NUnit, MSTest or any other test framework, because a failed check throws PdfAssertException,
which every framework reports as a failed test. Its only dependency is PdfPecker, released with it under the same
version, and the PDFs under test can come from PdfPecker, PdfPecker.Fluent or anything else.
using PdfPecker.Testing;
[Fact]
public void Invoice_looks_as_before()
{
byte[] pdf = InvoiceService.Render(SampleData.Invoice);
PdfAssert.MatchesSnapshot(pdf, "invoice"); // Snapshots/invoice.pdf next to this test file
PdfAssert.ContainsText(pdf, "Total due: 1,500.00");
PdfAssert.ContainsText(pdf, "Payment terms", pageNumber: 2);
PdfAssert.DoesNotContainText(pdf, "{{"); // no template placeholder left
PdfAssert.HasPageCount(pdf, 2);
}
Snapshots
The first run has no snapshot, so it fails and writes the PDF as Snapshots/invoice.received.pdf. Look at it, and
rename it to invoice.pdf to make it the snapshot. Commit the snapshots with the tests.
When a later PDF differs, the check fails with the first difference it found, such as the line of a content stream that changed, and writes next to the snapshot:
invoice.received.pdf, the new PDF;- for each page that looks different,
invoice.page-1.expected.png,invoice.page-1.actual.pngandinvoice.page-1.diff.png: the new page faded to grey with the changed pixels in magenta.
To accept a change, replace the snapshot with the received file, or run the tests with
PDFPECKER_UPDATE_SNAPSHOTS=1, which writes every missing or changed snapshot and passes. A check that passes deletes
the files an earlier failure left. Add the failure files to .gitignore:
*.received.pdf
*.page-*.expected.png
*.page-*.actual.png
*.page-*.diff.png
What has to match
By default the files' content is compared: every object in the same order, with streams inflated, and without dates,
file IDs and XMP document IDs. PdfPecker writes the same objects for the same input, so this catches every change,
including those that don't show on the page, such as a link target, a bookmark, the title or the text that copy and
paste and screen readers get. A PDF made again from the same input matches without a fixed CreationDate.
PdfSnapshotComparison.Appearance compares the rendered pages instead, for PDFs from other producers or when only the
look matters:
var options = new PdfSnapshotOptions
{
Comparison = PdfSnapshotComparison.Appearance,
ColorTolerance = 2, // a channel may differ by up to 2 of 255
MaxDifferentPixels = 50, // per page
};
PdfAssert.MatchesSnapshot(pdf, "invoice", options);
PdfSnapshotOptions |
Default | What it does |
|---|---|---|
Directory |
Snapshots next to the test's source file |
Where the snapshots and failure files are |
Comparison |
Content |
Content or Appearance, as above |
Dpi |
72 | Resolution of the compared and written page images |
ColorTolerance |
0 | How far a pixel's channel may differ before it counts as different |
MaxDifferentPixels |
0 | How many different pixels a page may have in Appearance comparisons |
UpdateSnapshots |
false | Write the PDF as the snapshot instead of failing |
WriteDiffImages |
true | Write the page images on a failure |
Password |
none | Password of encrypted PDFs |
The default folder comes from the source file path the compiler records for the calling test. A build with
ContinuousIntegrationBuild or PathMap records other paths, and the check then asks for Directory, such as
Path.Combine(AppContext.BaseDirectory, "Snapshots") with the snapshots copied to the output.
Text and pages
ContainsText, DoesNotContainText and HasPageCount read the PDF with PdfPecker's
text extraction. Line breaks and other runs of white space count as one space, so text that
wraps differently still matches.
Limitations
- The content comparison reads objects in file order, so a PDF whose producer orders or numbers its objects
differently on every run needs
Appearance. - Signed and encrypted PDFs change on every run by design; compare them with
Appearance, withPasswordfor encrypted ones. Appearancecompares PdfPecker's rendering of the pages, not a particular viewer's.