Getting started
This page takes you from an empty test project to an approved snapshot in about five minutes.
Requirements
- .NET 10 SDK.
- Any test framework. The examples use xUnit; NUnit and MSTest work the same way.
- Git, if you want snapshot changes to show up in code review (recommended).
1. Install
Add the library to your test project:
dotnet add package PDiffy
You can also install the command-line tool, which accepts snapshots, compares files and summarizes a CI run. Installed as a local tool at the same version as the package, it reads documents exactly as your tests do:
dotnet new tool-manifest
dotnet tool install PDiffy.Cli --version <the PDiffy package version>
A local tool runs as dotnet pdiffy.
2. Write a test
Snapshot.VerifyPdfAsync takes the PDF as a stream, prints its snapshot and compares it with the approved one.
using PDiffy;
public class TrailGuideTests
{
[Fact]
public async Task TrailGuide_ShouldMatchApprovedSnapshot_WhenGenerated()
{
// Your code that produces a PDF. A FileStream works too.
using Stream pdf = await ReportGenerator.CreateTrailGuideAsync();
await Snapshot.VerifyPdfAsync(pdf);
}
}
The snapshot is named after the test file and method, and sits next to the test file:
TrailGuideTests.TrailGuide_ShouldMatchApprovedSnapshot_WhenGenerated.verified.pdfua.txt.
3. Run it and approve the snapshot
The first run has nothing to compare with, so it fails with "New PDF snapshot" and writes what it got to a
.received.pdfua.txt file. The failure gives the snapshot's size, section by section, so you know what you're about
to accept. When layout makes up most of it, the failure also names a profile that leaves layout out. Open the
received file, check that it describes your document, then accept it in any of these ways:
- Run
dotnet pdiffy accept --allto accept every received snapshot under the current folder. - Run the tests in accept mode:
PDIFFY_ACCEPT=1 dotnet test, or in PowerShell$env:PDIFFY_ACCEPT=1; dotnet test. - Rename the
.received.pdfua.txtfile to.verified.pdfua.txtby hand.
Run the tests again: they pass. Commit the .verified.pdfua.txt file. From now on, any change to what the document
says fails the test, and accepting it shows up in code review as a diff of that file.
Note
Accept mode is ignored on a build server (when CI, TF_BUILD, JENKINS_URL or TEAMCITY_VERSION is set), so a
pipeline can never approve its own snapshots.
4. Set up the repository
Add this line to .gitattributes, so approved snapshots keep LF line endings on every OS:
*.verified.* text eol=lf
Add this to .gitignore. It covers the received snapshot, the report and any kept document, which only exist while a
test is failing:
*.received.*
What happens on each run
| Situation | PDiffy does | Test |
|---|---|---|
| No verified file yet | Writes the received file | Fails: "New PDF snapshot", with its size by section (4 pages; 1,362 lines: layout 1,240 (91%), structure 58, ...) and, when layout is most of it, a hint about the Content and Accessibility profiles |
| Snapshot differs | Writes the received file and an HTML report (.received.diff.html) |
Fails with a summary of what changed and where |
| Snapshot matches | Deletes any received file or report left from an earlier run | Passes |
PDIFFY_ACCEPT=1 is set |
Writes the snapshot as the new verified file | Passes |
PDIFFY_ACCEPT=1 is set on a build server |
Ignores it | Fails as usual |
Snapshot differs and PDIFFY_OPEN_REPORT=1 is set, not on a build server |
Also opens the report in the default browser | Fails as usual |
A PDF or Word snapshot fails and PDIFFY_KEEP_DOCUMENT=1 is set, or Snapshot.KeepDocument is true |
Also writes the document the test verified, if it was given as a stream or bytes, as {name}.received.pdf or .received.docx to open in a viewer. Deletes it once the snapshot passes |
Fails as usual |
| A PDF or Word snapshot fails on a build server | Keeps the document the same way unless PDIFFY_KEEP_DOCUMENT=0 is set or Snapshot.KeepDocument is false, since a document made there can be hard to make again |
Fails as usual |
Keep snapshots in one folder
Snapshots sit next to the test file by default. To keep them all in one folder, set it once, for example in a module initializer:
using System.Runtime.CompilerServices;
using PDiffy;
static class SnapshotSetup
{
[ModuleInitializer]
public static void Initialize() =>
Snapshot.Directory = Snapshot.ProjectFolder("Snapshots");
}
Snapshot.ProjectFolder gives a folder in the test project. PDiffy's build file
records the project's folder when it builds, so the path is right whatever the output folder or runtime, as long as the
tests run in the checkout that built them.
Note
If your build maps source paths (deterministic builds, common on CI), the path the compiler records for a test file isn't on disk. PDiffy finds the test file's folder in the recorded project folder instead, so snapshots next to test files still work.
Check every document
The built-in checks run only when a test asks for them. To record what they find in every snapshot, turn them on in the same module initializer, with a rule set that an upgrade doesn't change:
[ModuleInitializer]
public static void Initialize()
{
Snapshot.Directory = Snapshot.ProjectFolder("Snapshots");
Snapshot.DefaultPdfOptions = options => options.Check(PdfCheckSet.Version1_0);
Snapshot.DefaultDocxOptions = options => options.Check(DocxCheckSet.Version1_0);
}
Each snapshot then ends with a checks section, so a test fails when a finding appears or goes away. A finding you
can't fix yet is approved with the snapshot like any other line. Write why in a
notes file, and pdiffy findings --notes findings-notes.txt
lists the findings that have no note.
Without a snapshot file
Everything PDiffy reads is also available as properties, so you can assert on the parts you care about:
PdfUaDocument document = PdfUaDocument.Load("trail-guide.pdf");
Assert.True(document.Document.Marked);
Assert.Equal("en-US", document.Document.Language);
Assert.All(document.Elements(PdfStructureTypes.Figure), figure => Assert.False(string.IsNullOrWhiteSpace(figure.AltText)));
// The same text VerifyPdfAsync compares.
string snapshot = document.ToSnapshot();
Each structure element also has its text including its descendants' (AllText), the text runs it draws (TextRuns,
with their font, size and color), its descendants (Descendants(), Elements(type)) and its Path, the same path a
check finding shows. document.Headings() lists every heading with its level. The document's Elements(type) throws
for a type that's not standard, not in the role map and not used in the document, so a typo such as "figure" fails
instead of matching nothing and passing silently. An element's Elements(type) doesn't check, so use the
PdfStructureTypes constants.
Next steps
- PDF snapshots: reading a failure, the HTML report, naming and folders.
- Ignore and scrub: leave out dates, IDs, layout or whole sections.
- Continuous integration: keep the received files and reports from a failed run.