Blog

  • Home /
  • Blog /
  • Implementing Approval Tests For PDF Document Generation

Implementing Approval Tests For PDF Document Generation

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.

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 checkout the books section. Feel free to reach out at infonull@nullprincipal-itnull.be.

Profile picture of Jan Van Ryswyck

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

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.

Contact information

(+32) 496 38 00 82

infonull@nullprincipal-itnull.be