Every test mocked the helper, and except Exception swallowed its TypeError on every PDF
For seven weeks, a document classifier never read a single PDF. This is the helper that was supposed to give the model some text (trimmed):
def _extract_excerpt(file_path: Path) -> str:
"""First-page text for PDFs, first ~8 kB for everything else."""
if file_path.suffix.lower() == ".pdf":
try:
pages = extract_pdf(file_path)
return (pages[0] if pages else "")[:EXCERPT_BYTES]
except Exception:
return ""
...
extract_pdf doesn’t return a list of pages. It returns a dataclass, so every call raised this:
TypeError: 'PDFExtraction' object is not subscriptable
The except caught it and returned "". The prompt then held the filename and an empty excerpt, and the model answered anyway: a document type and a confidence score, for every PDF it was asked about.
If a helper of yours has a catch-all except and your tests patch that helper, check whether anything ever runs it for real.
Setup #
A web upload went through a small Python script that sorted the file into one of the document kinds declared for an accounting period (received invoices, bank statements, and so on). With two or more possible kinds, it built a prompt from the filename plus the start of the document and asked a language model, which answered with JSON like {"kind": "...", "confidence": 0.9}. The PDF text came from an in-house extract_pdf() built on pdfplumber. Tests used the standard library’s unittest and unittest.mock.
What went wrong #
The helper was wrong from the day it was written. extract_pdf had returned a PDFExtraction dataclass with a pages list since its first commit, six weeks earlier, and its docstring said so. The if pages check didn’t catch the mistake: a dataclass without __len__ is always truthy, so the check passed and pages[0] raised.
The except was there so that a corrupt or encrypted upload wouldn’t break classification. It caught the TypeError just as readily and returned "", which is also the documented answer for an image upload with no text to read.
And every test patched the helper out. The tests that reached the model looked like this:
with patch("scripts.classify_upload.invoke_ai") as m, \
patch("scripts.classify_upload._extract_excerpt", return_value="Invoice 2026-001 ..."):
m.return_value = json.dumps({"kind": "received_invoices", "confidence": 0.92})
result = classify_file(Path("/tmp/x.pdf"), ["issued_invoices", "received_invoices"], "claude")
They covered the prompt plumbing and the JSON parsing, and none of them ever called _extract_excerpt. The implementation plan that specified the helper had the same broken line, and it specified these tests, mocks included. The code, the plan, and the tests agreed with each other, and none of them had been checked against extract_pdf.
The model hid the rest. A filename like invoice-0423.pdf can be enough for a right guess, and a wrong guess came back with a confidence score like any other, so nothing in the output said the excerpt was empty.
Fix #
The fix reads the first page, as the docstring promised:
extraction = extract_pdf(file_path)
first_page = extraction.pages[0].text if extraction.pages else ""
return first_page[:EXCERPT_BYTES]
The new test class calls the real function on a real PDF:
class RealPdfExcerptTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
def tearDown(self):
self.tmp.cleanup()
def test_a_pdf_yields_its_first_page_text(self):
path = self.root / "invoice.pdf"
_write_pdf(path, "INVOICE 2026-001 Supplier ACME")
self.assertIn("INVOICE 2026-001", _extract_excerpt(path))
def test_a_corrupt_pdf_yields_no_signal_rather_than_raising(self):
path = self.root / "broken.pdf"
path.write_bytes(b"%PDF-1.4\nnot actually a pdf at all")
self.assertEqual(_extract_excerpt(path), "")
_write_pdf is an existing test helper that writes a one-page PDF with a text layer. Before committing, I ran the first test against the original code to confirm it fails there. The except stayed, because corrupt uploads are real. The second test now says what it’s for.
A day later, the same helper had a second route to the same result. It kept its own list of text suffixes, so Word, Excel, and XML e-invoice uploads fell through to "" and were classified by filename too. Two of those formats joined the pipeline a week after the list was written, and nobody updated it. The helper now asks the pipeline’s format registry instead.
Why #
A mock replaces the code it patches with your assumptions about that code. When every test mocks a function, its tests check only those assumptions, and the function itself is never run. A catch-all except inside the function then removes the last signal you’d get in production: no traceback, no failed request, just a default value that looks legitimate.
A language model downstream makes this worse. A parser fed an empty string usually fails loudly, while a model returns its best guess in the usual format, confidence field included, whatever the input.
Takeaway #
Mock the expensive or external dependency (here, the model) and let the function under test run on a real input. If it has a catch-all except, give it a test with an input that should succeed, so a bug can’t hide behind the except.
Grep your tests for patch( targets that point at your own module’s private helpers, and for each one, find a test that calls the helper unmocked.