DEV Community

Kamen
Kamen

Posted on Originally published at kamenivanov.substack.com

Our H2 tests passed but postgres in production disagreed

Cover

The test suite was green, as it had been for months. The migration ran clean on H2, the repository tests passed, the CI pipeline gave its usual thumbs up, and the change went out. A few hours later, a batch job that touched category records started failing in production with an error none of us recognized on sight: invalid input value for enum category_status. We didn't have that enum in our test environment. We had a VARCHAR column.

The status field had gone through a refactor earlier in the year, moving from a loose two-value flag to a proper three-value enum, ACTIVE, INACTIVE, ARCHIVED. On Postgres, that was implemented as a native enum type at the database level, which is the stricter, more correct way to do it, since the database itself now refuses anything outside the three allowed values. On H2, for reasons that made sense at the time and stopped making sense the day this happened, the equivalent migration script defined the column as a plain string. H2 doesn't support native Postgres enum types, so somewhere along the way the two migration scripts had quietly diverged, one for tests, one for production, and nobody had flagged it as a problem because both passed their respective test suites.

H2 vs Postgress difference

The batch job was writing a leftover status value from before the refactor, a string that used to be valid and no longer was. On H2, that string went into a VARCHAR column without complaint. On Postgres, the database did exactly what a native enum type is supposed to do and rejected it outright. The bug wasn't really in the batch job. It was in the fact that our tests had never once run against a schema that could actually catch this class of error, because the schema they ran against wasn't the same schema running in production.

2026-03-14 14:22:07.113 ERROR --- [batch-worker-3] o.h.engine.jdbc.spi.SqlExceptionHelper
org.postgresql.util.PSQLException: ERROR: invalid input value for enum category_status: "LEGACY_HIDDEN"
at CategoryStatusUpdateBatchJob.processRecord(CategoryStatusUpdateBatchJob.java:47)
at o.hibernate.exception.internal.SQLStateConversionDelegate.convert
H2 test run for this batch job: PASSED
Enter fullscreen mode Exit fullscreen mode

We moved that test suite onto Testcontainers, running the real Postgres migration script instead of an H2 substitute, and reran the batch job's test coverage against it. It failed on the first run. Ten minutes of investigation, and it was pointing at the exact same invalid enum value production had hit, except this time in a local test run instead of an incident channel.

The lesson isn't that H2 is bad, it's fast and convenient for a lot of testing. The lesson is that convenience has a boundary, and that boundary is anywhere your production database enforces something your test database doesn't know how to enforce. If your schema does real work at the database level, constraints, enums, exclusion rules, your tests need to run against a database willing to do that same work. Otherwise green tests are only telling you the code runs, not that it's correct.

Top comments (0)