Implementing Approval Tests For PDF Document Generation
December 15, 2021In 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.
If you and your team want to learn more about how to write maintainable unit tests and get the most out of TDD practices, make sure to have look at our trainings and workshops or check out the books section. Feel free to reach out at info. @ principal-it .be
Jan Van Ryswyck
Thank you for visiting my blog. I’m a professional software developer since Y2K. A blogger since Y2K+5. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Past organizer of the European Virtual ALT.NET meetings. Thinking and learning about all kinds of technologies since forever.
Comments
Writing Maintainable
Unit Tests
Watch The Videos
Latest articles
-
Contract Tests - Parameterised Test Cases
June 28, 2023
-
Contract Tests - Abstract Test Cases
April 12, 2023
-
Contract Tests
February 1, 2023
-
The Testing Quadrant
June 15, 2022
-
Tales Of TDD: The Big Refactoring
February 2, 2022
Tags
- .NET
- ALT.NET
- ASP.NET
- Agile
- Announcement
- Architecture
- Behavior-Driven Development
- C++
- CQRS
- Clojure
- CoffeeScript
- Community
- Concurrent Programming
- Conferences
- Continuous Integration
- Core Skills
- CouchDB
- Database
- Design Patterns
- Domain-Driven Design
- Event Sourcing
- F#
- Fluent Interfaces
- Functional Programming
- Hacking
- Humor
- Java
- JavaScript
- Linux
- Microsoft
- NHibernate
- NoSQL
- Node.js
- Object-Relational Mapping
- Open Source
- Reading
- Ruby
- Software Design
- SourceControl
- Test-Driven Development
- Testing
- Tools
- Visual Studio
- Web
- Windows
Disclaimer
The opinions expressed on this blog are my own personal opinions. These do NOT represent anyone else’s view on the world in any way whatsoever.
About
Thank you for visiting my website. I’m a professional software developer since Y2K. A blogger since Y2K+5. Author of Writing Maintainable Unit Tests. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Thinking and learning about all kinds of technologies since forever.
Latest articles
Contract Tests - Parameterised Test Cases
Contract Tests - Abstract Test Cases
Contract Tests
The Testing Quadrant
Contact information
(+32) 496 38 00 82
info @ principal-it .be