Testing · Reliability

The Bugs
Unit Tests Can't See

A mocked repository never opens a real Postgres connection. A fake HTTP client never triggers a real JDK retry bug. Four real bugs, and every one of them needed real infrastructure to even exist.

Every one of these bugs passed every unit test that existed at the time. That's not a failure of the unit tests — they were testing the things they could reach. The problem is that some failure modes only exist at the boundary between your code and something real: a real database connection lifecycle, a real HTTP client's retry logic, a real message queue's deduplication window. A fake stands in for the interface, not the failure mode.

The Lob Stream That Closed Too Early

Migrating the Outbox from a synchronous same-process drain to an async SQS-based poller/consumer split introduced a real regression, caught only during verification against a real database: OutboxPoller.poll() was missing @Transactional. The payload column is a JPA @Lob, loaded lazily — and without the method itself being a transaction boundary, the session used for the query was already closed by the time the loop tried to stream the payload back out.

// Why @Transactional is needed: OutboxEvent.payload, loaded by
// findByProcessedFalseOrderByCreatedAtAsc(), is an @Lob column — if this method itself isn't a
// transaction boundary, the session/connection used for the query is already returned by the
// time the loop below tries to lazily stream event.getPayload(), causing an
// "Unable to access lob stream" exception and silently publishing nothing.
@Scheduled(fixedDelay = 1000)
@Transactional
public void poll() { /* ... */ }

A mocked repository returns a plain in-memory object; it has no session to close, no LOB to stream, and no way to reproduce this. It needed a real Hibernate session against a real Postgres connection, closing at a real transaction boundary, before this exception could exist at all.

The 401 Nobody Had Actually Tested Before

Fixing the authentication bypass covered in an earlier post meant writing, for the first time in either Spring Boot port, a test that actually asserts a real 401 response. That test immediately hit a different bug: Spring's default TestRestTemplate request factory sits on top of the JDK's own HttpURLConnection, which throws IOException: cannot retry due to server authentication, in streaming mode the moment a POST gets a 401 back — a known limitation of the JDK client itself, not of the code under test.

@BeforeEach
void useApacheHttpClientRequestFactory() {
    // The default JDK HttpURLConnection-based factory can't handle a 401 response to a POST.
    // Swap in the httpclient5-based factory, which doesn't have this limitation.
    restTemplate.getRestTemplate().setRequestFactory(new HttpComponentsClientHttpRequestFactory())
}

No mock or fake HTTP client runs the JDK's actual request/response state machine. This bug is in that state machine — it only exists when a real socket, a real running server, and a real 401 are all in the loop together.

Never Merged, Still a Real Lesson

Not every bug in this category shipped in production code. A benchmark run comparing all five language ports against a "recurring transfer" feature spec — deliberately never merged, since there was no real caller for the feature yet — surfaced two more, recorded as a first-person engineering log rather than a fix commit:

In the Go port, a reference ID built from a 32-character hex ID plus a -YYYY-MM suffix (40 characters) was written into a reference_id VARCHAR(36) column. Postgres rejected it — but only starting on the second month's run, since the first insert of any given length pattern can coincidentally fit. An in-memory fake repository doesn't enforce column-length constraints at all, so nothing before real Postgres could have caught it.

In the java-springboot port, three separate @Test methods each called the same monthly scheduler within the same test run. The scheduler's dedup ID was date-based at month granularity — identical for all three calls — and SQS FIFO's five-minute deduplication window silently dropped the second and third. Only the first test's Task actually reached the queue.

An unmerged bug that still changed real code

The VARCHAR(36) lesson didn't stay theoretical. A real, merged account-transfer feature shipped the same day, and its Go implementation explicitly avoids the exact trap the benchmark surfaced — using the raw 32-character ID with no suffix at all, specifically because appending one could exceed the column limit. A benchmark run whose code was thrown away still produced a lesson that shaped production code the same afternoon.

What All Four Have in Common

None of these are exotic. A missing annotation, a client library's known limitation, a column-length constraint, a deduplication window — ordinary infrastructure behavior, not edge cases dreamed up to stress-test a system. What they share is that a mock or fake, by construction, doesn't implement the actual failure surface: no real session to close early, no real HTTP client state machine, no real column, no real dedup window. Confidence that a feature works has to include running it, at least once, against the real things it depends on — not because unit tests are wrong, but because they were never testing this part of the system.

Further reading in the repo

docs/architecture/testing.md — the Domain/Application/E2E testing strategy these bugs fell outside of · OutboxPoller.java — the real fix for the Lob-stream bug · docs/benchmark.md — the first-person log of the two unmerged benchmark bugs