Implementing Approval Tests For PDF Document Generation
Jan Van Ryswyck
Posted on December 15, 2021
In the previous blog post, we discussed how to use Approval Tests for verifying generated PDF documents. In this blog post I’m going to show how to extend the Approval Test library for Java in order to support PDF documents. Let’s just dive right into the code.
The first thing that needs to happen is making a new implementation of the ApprovalApprover
interface, which is provided by the Approval Test library. The following code demonstrates how this can be implemented.
public class PdfFileApprover implements ApprovalApprover {
public static final Field<String> PDF_DIFF_OUTPUT_DIRECTORY =
new Field<>("PdfDiffOutputDirectory", String.class);
private final ApprovalNamer namer;
private final ApprovalWriter writer;
private final double allowedDiffInPercent;
private final List<PageArea> excludedAreas;
private File received;
private final File approved;
public PdfFileApprover(ApprovalWriter writer, PdfFileOptions options) {
this.writer = writer;
this.allowedDiffInPercent = options.getAllowedDiffInPercent();
this.excludedAreas = options.getExcludedAreas();
namer = options.getParent().forFile().getNamer();
received = namer.getReceivedFile(writer.getFileExtensionWithDot());
approved = namer.getApprovedFile(writer.getFileExtensionWithDot());
}
public VerifyResult approve()
{
received = writer.writeReceivedFile(received);
return approvePdfFile(received, approved);
}
public void cleanUpAfterSuccess(ApprovalFailureReporter reporter)
{
received.delete();
if(reporter instanceof ApprovalReporterWithCleanUp) {
((ApprovalReporterWithCleanUp) reporter)
.cleanUp(received.getAbsolutePath(), approved.getAbsolutePath());
}
}
public VerifyResult reportFailure(ApprovalFailureReporter reporter)
{
reporter.report(received.getAbsolutePath(), approved.getAbsolutePath());
if (reporter instanceof ReporterWithApprovalPower)
{
ReporterWithApprovalPower reporterWithApprovalPower = (ReporterWithApprovalPower) reporter;
return reporterWithApprovalPower.approveWhenReported();
}
return VerifyResult.FAILURE;
}
public void fail()
{
throw new Error(String.format("Failed Approval\n Approved:%s\n Received:%s",
approved.getAbsolutePath(), received.getAbsolutePath()));
}
private VerifyResult approvePdfFile(File received, File approved) {
try {
SimpleEnvironment environment = new SimpleEnvironment();
environment.setAllowedDiffInPercent(this.allowedDiffInPercent);
PdfComparator<CompareResultImpl> pdfComparator = new PdfComparator<>(approved, received)
.withEnvironment(environment);
excludedAreas.forEach(pdfComparator::withIgnore);
CompareResultImpl comparisonResult = pdfComparator.compare();
if(comparisonResult.isNotEqual()) {
String outputFileName = determineDiffOutputFileName();
comparisonResult.writeTo(outputFileName);
}
return VerifyResult.from(comparisonResult.isEqual());
} catch (IOException e) {
return VerifyResult.FAILURE;
}
}
private String determineDiffOutputFileName() {
var outputDirectory = PackageLevelSettings.getValueFor(PDF_DIFF_OUTPUT_DIRECTORY);
if(null == outputDirectory) {
outputDirectory = namer.getSourceFilePath();
}
return Path.of(outputDirectory, "diffOutput").toString();
}
}
The PdfFileApprover
class provides the core implementation for supporting PDF documents. Most of the code is quite similar to the FileApprover
class of the Approval Test library. However, the approvePdfFile
method is the most important part. This method expects two arguments; the received PDF file and the approved PDF file. The purpose of the approvePdfFile
method is to compare both incoming PDF files. For doing the actual comparison we make use of the PDFCompare library.
When there’s a difference between these PDF files, we save the result of the comparison to a PDF file so that we can visually inspect the differences as well as instruct the framework to fail the test. Also notice that the PDFCompare
library has the ability to exclude certain areas within a PDF file from the comparison as well as allowing a certain percentage of differences.
public class PdfApprovals {
public static void verify(ByteArrayOutputStream outputStream) {
verify(outputStream, PdfFileOptions.DEFAULT_ALLOWED_DIFF_IN_PERCENT, Collections.emptyList());
}
public static void verify(ByteArrayOutputStream outputStream, List<PageArea> excludedAreas) {
verify(outputStream, PdfFileOptions.DEFAULT_ALLOWED_DIFF_IN_PERCENT, excludedAreas);
}
public static void verify(ByteArrayOutputStream outputStream, double allowedDiffInPercentage) {
verify(outputStream, allowedDiffInPercentage, Collections.emptyList());
}
public static void verify(ByteArrayOutputStream outputStream, double allowedDiffInPercentage,
List<PageArea> excludedAreas) {
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
ApprovalBinaryFileWriter binaryFileWriter = new ApprovalBinaryFileWriter(inputStream, "pdf");
PdfFileOptions options = new PdfFileOptions()
.withAllowedDiffInPercent(allowedDiffInPercentage)
.withExcludedAreas(excludedAreas);
PdfApprovals.verify(binaryFileWriter, options);
}
private static void verify(ApprovalWriter writer, PdfFileOptions options) {
PdfFileApprover pdfFileApprover = new PdfFileApprover(writer, options);
Approvals.verify(pdfFileApprover, options.getParent());
}
}
The PdfApprovals
class provides a number of static helper methods that can be used by the tests themselves. These helper methods ultimately use the PdfFileApprover
class to perform the actual comparison. A number of overloaded methods are available to provide the ability of excluding certain areas and/or tweaking the allowed percentage of differences when performing the comparison.
These can be used as follows:
PdfApprovals.verify(result);
...
PdfApprovals.verify(result, 0.18);
...
var excludedAreas = Arrays.asList(
new PageArea(4, 4, 12, 18),
new PageArea(35, 38, 42, 45)
);
PdfApprovals.verify(result, excludedAreas);
...
PdfApprovals.verify(result, 0.18, excludedAreas);
For completeness, the following code shows the implementation of the PdfFileOptions
and PackageSettings
classes. These are necessary to provide the PdfFileApprover
class with the necessary configuration settings for performing the comparison and saving the output files.
public class PdfFileOptions {
public static final double DEFAULT_ALLOWED_DIFF_IN_PERCENT = 0.001;
private enum CustomFields {
ALLOWED_DIFF_IN_PERCENT,
EXCLUDED_AREAS
}
private final Map<CustomFields, Object> customFields;
private final Options options;
public PdfFileOptions() {
customFields = new HashMap<>();
customFields.put(CustomFields.ALLOWED_DIFF_IN_PERCENT, DEFAULT_ALLOWED_DIFF_IN_PERCENT);
customFields.put(CustomFields.EXCLUDED_AREAS, Collections.emptyList());
options = new Options();
options.forFile().withExtension(".pdf");
}
public double getAllowedDiffInPercent() {
return (double) customFields.get(CustomFields.ALLOWED_DIFF_IN_PERCENT);
}
public List<PageArea> getExcludedAreas() {
return (List<PageArea>) customFields.get(CustomFields.EXCLUDED_AREAS);
}
public Options getParent() {
return options;
}
public PdfFileOptions withAllowedDiffInPercent(double allowedDiffInPercent) {
customFields.put(CustomFields.ALLOWED_DIFF_IN_PERCENT, allowedDiffInPercent);
return this;
}
public PdfFileOptions withExcludedAreas(List<PageArea> excludedAreas) {
customFields.put(CustomFields.EXCLUDED_AREAS, excludedAreas);
return this;
}
}
public class PackageSettings {
public static String ApprovalBaseDirectory = "../resources";
public static String PdfDiffOutputDirectory =
String.format("%s/build/tmp", System.getProperty("user.dir"));
}
That’s all there is to it. With just this tiny bit of code we’re able to use Approval Tests for PDF documents.
Posted on December 15, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.