Unit and Integration Testing PDF Generation with iText 7 in Java
iText 7 is the most widely used Java PDF generation library. Testing iText-generated PDFs requires writing to an in-memory stream, parsing the output with iText's PdfReader, and asserting on text content, layout, fonts, and structure. This guide covers JUnit 5 patterns for all of these.
Key Takeaways
Write PDFs to a ByteArrayOutputStream for tests. Never write test output to real files — use in-memory streams for fast, side-effect-free tests.
Use PdfTextExtractor to assert on content. iText's PdfTextExtractor.getTextFromPage() extracts the text layer per page — the primary assertion method for content tests.
Test table cell content by location, not just presence. The text "100.00" might be a price, a quantity, or a page number. Test which column a value appears in by using location-based extraction.
Test fonts separately from content. Missing font embedding causes garbled text in some viewers. Assert that required fonts are embedded in the PDF resources.
Use iText's CompareTool for visual regression. CompareTool.compareByContent() provides a reliable byte-level PDF comparison built into iText — better than custom pixel diffing.
iText 7 Testing Architecture
iText 7 generates PDFs programmatically. A typical generation class looks like:
public class InvoiceGenerator {
public byte[] generate(Invoice invoice) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf, PageSize.A4);
document.add(new Paragraph("Invoice #" + invoice.getNumber())
.setFontSize(20)
.setBold());
document.add(buildLineItemsTable(invoice.getLineItems()));
document.add(new Paragraph("Total: $" + invoice.getTotal())
.setBold()
.setTextAlignment(TextAlignment.RIGHT));
document.close();
return baos.toByteArray();
}
private Table buildLineItemsTable(List<LineItem> items) {
Table table = new Table(new float[]{4, 1, 1, 1});
table.addHeaderCell("Description");
table.addHeaderCell("Qty");
table.addHeaderCell("Rate");
table.addHeaderCell("Total");
for (LineItem item : items) {
table.addCell(item.getDescription());
table.addCell(String.valueOf(item.getQuantity()));
table.addCell("$" + item.getRate());
table.addCell("$" + item.getTotal());
}
return table;
}
}Basic JUnit 5 Setup
Add iText 7 and test dependencies to pom.xml:
<dependencies>
<!-- iText 7 Core -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.2.5</version>
<type>pom</type>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>Testing PDF Content with PdfTextExtractor
The primary content assertion mechanism:
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor;
import org.junit.jupiter.api.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;
class InvoiceGeneratorTest {
private InvoiceGenerator generator;
private Invoice sampleInvoice;
@BeforeEach
void setUp() {
generator = new InvoiceGenerator();
sampleInvoice = Invoice.builder()
.number("INV-2026-001")
.clientName("Acme Corp")
.addLineItem("Web Development Services", 40, 150.0)
.addLineItem("Design Consultation", 5, 200.0)
.build();
}
@Test
void generatesNonEmptyPdfBuffer() throws IOException {
byte[] pdfBytes = generator.generate(sampleInvoice);
assertNotNull(pdfBytes);
assertTrue(pdfBytes.length > 1000, "PDF should be at least 1KB");
// Verify PDF header
String header = new String(pdfBytes, 0, 4);
assertEquals("%PDF", header);
}
@Test
void containsInvoiceNumber() throws IOException {
byte[] pdfBytes = generator.generate(sampleInvoice);
String pageText = extractPageText(pdfBytes, 1);
assertTrue(pageText.contains("INV-2026-001"),
"PDF should contain the invoice number");
}
@Test
void containsClientName() throws IOException {
byte[] pdfBytes = generator.generate(sampleInvoice);
String pageText = extractPageText(pdfBytes, 1);
assertTrue(pageText.contains("Acme Corp"));
}
@Test
void containsAllLineItemDescriptions() throws IOException {
byte[] pdfBytes = generator.generate(sampleInvoice);
String pageText = extractPageText(pdfBytes, 1);
assertTrue(pageText.contains("Web Development Services"));
assertTrue(pageText.contains("Design Consultation"));
}
@Test
void showsCorrectTotal() throws IOException {
byte[] pdfBytes = generator.generate(sampleInvoice);
String pageText = extractPageText(pdfBytes, 1);
// 40*150 + 5*200 = 7000
assertTrue(pageText.contains("7,000") || pageText.contains("7000"),
"PDF should show correct total of 7000");
}
private String extractPageText(byte[] pdfBytes, int pageNumber) throws IOException {
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
return PdfTextExtractor.getTextFromPage(pdfDoc.getPage(pageNumber));
}
}
}Testing Page Count
@Test
void singleInvoiceFitsOnOnePage() throws IOException {
byte[] pdfBytes = generator.generate(sampleInvoice);
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
assertEquals(1, pdfDoc.getNumberOfPages(),
"Standard invoice should fit on one page");
}
}
@Test
void longInvoiceSpansMultiplePages() throws IOException {
Invoice largeInvoice = Invoice.builder()
.number("INV-LARGE")
.clientName("Big Corp")
.lineItems(generateLineItems(100))
.build();
byte[] pdfBytes = generator.generate(largeInvoice);
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
assertTrue(pdfDoc.getNumberOfPages() > 1,
"100-item invoice should span multiple pages");
}
}Testing PDF Metadata
@Test
void pdfHasCorrectTitle() throws IOException {
byte[] pdfBytes = generator.generate(sampleInvoice);
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
PdfDocumentInfo info = pdfDoc.getDocumentInfo();
assertEquals("Invoice INV-2026-001", info.getTitle());
}
}
@Test
void pdfHasCreatorSet() throws IOException {
byte[] pdfBytes = generator.generate(sampleInvoice);
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
PdfDocumentInfo info = pdfDoc.getDocumentInfo();
assertEquals("HelpMeTest Billing", info.getCreator());
}
}Testing Fonts
Missing font embedding is a common production bug. Test that required fonts are embedded:
@Test
void customFontIsEmbedded() throws IOException {
byte[] pdfBytes = generator.generate(sampleInvoice);
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
// Check page 1 resources for embedded fonts
PdfPage page = pdfDoc.getPage(1);
PdfDictionary resources = page.getResources().getPdfObject();
PdfDictionary fonts = resources.getAsDictionary(PdfName.Font);
assertNotNull(fonts, "Page should have font resources");
boolean hasEmbeddedFont = false;
for (PdfName key : fonts.keySet()) {
PdfDictionary font = fonts.getAsDictionary(key);
PdfDictionary descriptor = font.getAsDictionary(PdfName.FontDescriptor);
if (descriptor != null && descriptor.get(PdfName.FontFile2) != null) {
hasEmbeddedFont = true;
break;
}
}
assertTrue(hasEmbeddedFont,
"PDF should have at least one embedded font to prevent rendering issues");
}
}Testing Tables with Location-Based Extraction
When the same value could appear in different columns, use iText's location-based text extraction:
import com.itextpdf.kernel.pdf.canvas.parser.listener.LocationTextExtractionStrategy;
import com.itextpdf.kernel.geom.Rectangle;
@Test
void priceColumnShowsCorrectValues() throws IOException {
byte[] pdfBytes = generator.generate(sampleInvoice);
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
// Extract text from the "Rate" column region (right side of page)
// A4 = 595 x 842 points; columns vary by layout
Rectangle rateColumnRegion = new Rectangle(400, 500, 80, 300);
String rateColumnText = extractRegionText(pdfDoc.getPage(1), rateColumnRegion);
assertTrue(rateColumnText.contains("150"));
assertTrue(rateColumnText.contains("200"));
}
}
private String extractRegionText(PdfPage page, Rectangle region) throws IOException {
FilteredTextEventListener strategy = new FilteredTextEventListener(
new LocationTextExtractionStrategy(),
new TextRegionEventFilter(region)
);
return PdfTextExtractor.getTextFromPage(page, strategy);
}Regression Testing with CompareTool
iText provides CompareTool for PDF comparison. This is more reliable than pixel-level image comparison:
import com.itextpdf.kernel.utils.CompareTool;
import java.nio.file.Files;
import java.nio.file.Path;
@Test
void invoicePdfMatchesBaseline() throws IOException, InterruptedException {
Path outputDir = Path.of("build/test-output/pdfs");
Files.createDirectories(outputDir);
Path generatedPath = outputDir.resolve("invoice-current.pdf");
Path baselinePath = Path.of("src/test/resources/baselines/invoice-baseline.pdf");
byte[] pdfBytes = generator.generate(sampleInvoice);
Files.write(generatedPath, pdfBytes);
if (!Files.exists(baselinePath)) {
// First run: create baseline
Files.createDirectories(baselinePath.getParent());
Files.copy(generatedPath, baselinePath);
System.out.println("Baseline created at: " + baselinePath);
return;
}
CompareTool compareTool = new CompareTool();
String compareResult = compareTool.compareByContent(
generatedPath.toString(),
baselinePath.toString(),
outputDir.toString(),
"diff_"
);
assertNull(compareResult,
"PDF differs from baseline: " + compareResult);
}Testing PDF/A Compliance
For regulated industries, PDF/A compliance is mandatory. Test it explicitly:
import com.itextpdf.pdfa.PdfADocument;
import com.itextpdf.kernel.pdf.PdfOutputIntent;
import com.itextpdf.kernel.pdf.PdfAConformanceLevel;
@Test
void generatesPdfACompliantDocument() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Use PdfADocument instead of PdfDocument
PdfADocument pdf = new PdfADocument(
new PdfWriter(baos),
PdfAConformanceLevel.PDF_A_1B,
new PdfOutputIntent(
"Custom", "", "http://www.color.org",
"sRGB IEC61966-2.1",
getClass().getResourceAsStream("/sRGB_CS_profile.icm")
)
);
Document document = new Document(pdf);
document.add(new Paragraph("PDF/A Compliant Content"));
document.close();
byte[] pdfBytes = baos.toByteArray();
// Verify PDF/A header
try (PdfDocument readDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
assertNotNull(readDoc.getXmpMetadata(),
"PDF/A document must have XMP metadata");
// Verify conformance level
String xmpXml = new String(readDoc.getXmpMetadata().getXmpMetadataAsBytes());
assertTrue(xmpXml.contains("PDF/A-1"),
"XMP metadata should declare PDF/A-1 conformance");
}
}Parameterized Tests
Use JUnit 5 parameterized tests for testing multiple invoice types:
@ParameterizedTest
@MethodSource("invoiceVariants")
void allInvoiceTypesGenerateValidPdf(String name, Invoice invoice, int expectedPages)
throws IOException {
byte[] pdfBytes = generator.generate(invoice);
assertNotNull(pdfBytes);
try (PdfDocument doc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
assertEquals(expectedPages, doc.getNumberOfPages(),
name + " should have " + expectedPages + " pages");
}
}
static Stream<Arguments> invoiceVariants() {
return Stream.of(
Arguments.of("minimal", minimalInvoice(), 1),
Arguments.of("standard", standardInvoice(), 1),
Arguments.of("large", largeInvoice(100), 3)
);
}Testing Error Handling
@Test
void throwsOnNullInvoice() {
assertThrows(NullPointerException.class,
() -> generator.generate(null));
}
@Test
void handlesEmptyLineItemList() throws IOException {
Invoice emptyInvoice = Invoice.builder()
.number("INV-EMPTY")
.clientName("Test Client")
.lineItems(Collections.emptyList())
.build();
byte[] pdfBytes = generator.generate(emptyInvoice);
// Should generate a valid PDF with an empty table
assertNotNull(pdfBytes);
String text = extractPageText(pdfBytes, 1);
assertTrue(text.contains("INV-EMPTY"));
}
@Test
void handlesSpecialCharactersInContent() throws IOException {
Invoice specialInvoice = Invoice.builder()
.number("INV-SPECIAL")
.clientName("Müller & Söhne GmbH")
.addLineItem("Café consulting — €2000/day", 1, 2000.0)
.build();
byte[] pdfBytes = generator.generate(specialInvoice);
String text = extractPageText(pdfBytes, 1);
// May require Unicode font support
assertTrue(text.contains("Müller") || text.contains("Muller"));
}CI Configuration
name: iText PDF Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Cache Maven
uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- run: mvn test
- name: Upload PDF test artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: pdf-test-output
path: build/test-output/pdfs/Summary
iText 7 PDF testing in Java:
| Concern | Tool | Method |
|---|---|---|
| Content | PdfTextExtractor |
getTextFromPage() |
| Page count | PdfDocument |
getNumberOfPages() |
| Metadata | PdfDocumentInfo |
getTitle(), getCreator() |
| Fonts | PdfPage resources |
Font descriptor inspection |
| Layout | LocationTextExtractionStrategy |
Region-based extraction |
| Regression | CompareTool |
compareByContent() |
| Compliance | PdfADocument |
XMP metadata + conformance |
The missing piece after these tests: verifying that a PDF download button in your application actually triggers the download and serves a valid file. For that, HelpMeTest provides browser-level E2E test coverage for the full document generation flow from user action to downloaded file.