Article
2 min read
15,000 Database Integration Tests in 20 Minutes — on a Single Machine?

Author
Tomasz Bialecki
Last Update
September 02, 2026

Table of Contents
The problem: integration tests hate concurrency
The idea: migrate once, fork forever
What each of those 15k tests actually does
Why it matters
No mocks. No shared fixtures. No "reset the database between suites" bottleneck. Every one of those tests seeds its own rows into a real Postgres, calls a real HTTP endpoint through the full authentication stack, and asserts on the actual response. And they all run concurrently, on one box.
The trick is a feature Postgres has had for decades and almost nobody uses: template databases.
The problem: integration tests hate concurrency
Real integration tests want a real database. But a real database is shared, mutable state — the one thing concurrent tests cannot tolerate. The classic escapes all hurt:
- Mock the database — now you're testing your mocks, not your queries, constraints, or triggers.
- Wrap each test in a rolled-back transaction — breaks the moment the code under test manages its own transactions, or the assertion needs to see committed data.
- Truncate everything between tests — forces the whole suite into a single file line, so the wall-clock time grows linearly with every test you add.
- One database per test, migrated from scratch — correct, but running hundreds of migrations per test file takes minutes each. Nobody waits for that.
So most teams settle: a thin layer of "unit" tests with mocked repositories, and a handful of slow, flaky end-to-end tests. The middle — fast, honest, database-backed tests at scale — stays empty.
The idea: migrate once, fork forever
Postgres lets any database be marked as a template. CREATE DATABASE foo WITH TEMPLATE bar doesn't replay any SQL — it copies the template's files at the filesystem level. For a schema-plus-seed-data database that takes on the order of a couple hundred milliseconds, no matter how many migrations it took to build.
That inverts the cost structure completely:
- Once per CI run (pnpm db-template:init): create deel_test_template, run the full migration history against it — the monolith migrations plus the permission migrations — load the static seed data (countries, currencies, payroll configuration), and lock it with ALTER DATABASE ... is_template=true.
- Once per test file: fork it. A beforeAll hook hashes the test file's path into a database name (deel_test_db_) and runs CREATE DATABASE deel_test_db_ab12cd34ef56 WITH TEMPLATE deel_test_template. The afterAll hook drops it.
Every test file now owns a private, fully migrated, fully seeded database. It can insert whatever it wants, leave it dirty, even crash — nothing leaks to any other file. The name is derived from the file path, so it's stable and collision-free; the drop routine refuses to touch anything not prefixed deel_test_db_, so a bug can never take out a real database.
Because there is no shared state left, the test runner is free to parallelize. Vitest runs the files in forked worker processes (maxWorkers, 4 by default, tunable via an env var), and Nx runs the test targets of every package in the monorepo alongside each other. Two layers of parallelism, zero coordination between tests — the database stopped being the bottleneck, so the machine's cores became the limit.
Keeping the template fresh is cheap too: a sync command unlocks it, applies only the pending migrations, and locks it again — with the re-lock in a finally block, so even a failed migration can't leave the template droppable.
What each of those 15k tests actually does
These aren't "call the function, check the return value" tests. A typical one:
- Seeds real data through a fluent seeder — an organization, a contractor, a signed contract, an HRIS profile — as ordinary inserts into its private database.
- Issues a real HTTP request with a test client that mints a genuine session token — as a client, a worker, or even an admin impersonation session — and sends it through the full NestJS stack: guards, authentication, permission checks, controller, service, mapper.
- Asserts on the wire response — the actual JSON a frontend would receive, status code included.

Foreign keys are enforced. Unique constraints fire. The permission engine resolves real role assignments from real rows. When one of these tests goes green, the endpoint works — not "works assuming the mocks were right."
Why it matters
The template framework removed the trade-off between honesty and speed. Tests that exercise the real database and the real auth stack used to be a scarce resource, budgeted and rationed; now they're the default way to test an endpoint, and the suite scales by adding cores rather than by adding hours. Fifteen thousand full-stack database tests in twenty minutes on a single machine isn't a stunt — it's just what happens when forking a database costs less than a network round-trip.

Staff engineer at Deel. Passionate about software development, clean architecture, clean code and pragmatism.