Implementing Approval Tests For PDF Document Generation
Tags: unittests, tdd, testing, softwaredesign
Published: 2021-12-15 09:24:26

In the previous blog post, we discussed how to use [Approval Tests for verifying generated PDF documents](https://dev.to/janvanryswyck/approval-tests-for-pdf-document-generation-1ceb). In this blog post I’m going to show how to extend the [Approval Test library for Java](https://github.com/approvals/ApprovalTests.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.

```java
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](https://github.com/red6/pdfcompare).

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.

```java
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:

```java
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.

```java
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.