Disposable databases with Testcontainers
If your tests hit the same Postgres your dev server uses, they create fake data, delete your seed books, and collide with anything you are working on. Two developers running tests at the same time make it worse. You want a completely fresh database every time the suite runs.
Testcontainers is a library that starts a real Postgres container from your test code, waits for it to be ready, and gives you the connection string. When the suite ends, the container is destroyed and the data is gone. Zero contamination, perfect isolation, no lingering state.
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { execSync } from 'child_process';
let container: StartedPostgreSqlContainer;
beforeAll(async () => {
// 1. Start the container
container = await new PostgreSqlContainer('postgres:15-alpine')
.withDatabase('bookstore_test')
.withUsername('testuser')
.withPassword('testpass')
.start();
// 2. Override env BEFORE the app loads
const uri = container.getConnectionUri();
process.env.DATABASE_URL = uri;
process.env.JWT_SECRET = 'test_secret_key';
// 3. Push the Drizzle schema into the fresh database
execSync('npx drizzle-kit push', {
env: { ...process.env, DATABASE_URL: uri },
stdio: 'ignore',
});
// 4. Dynamically import the app so it reads the new env
const mod = await import('../after/index.js');
app = mod.app;
}, 60000); // allow 60s the first time, in case Docker pulls the image
afterAll(async () => {
if (container) await container.stop();
});Start a fresh Postgres container, point DATABASE_URL at it, push the schema, then dynamically import the app.
Because your app reads DATABASE_URL the moment it loads. If you use a normal import at the top, that happens before beforeAll runs, so the app connects to the wrong database. Dynamic import runs at the point you call it, after the env is already configured. Set the env, then import. That order is the entire trick.
One operational detail: Testcontainers needs Docker Desktop running on your machine. The first time a test suite runs, it pulls the postgres image, which can take a minute. After that, startup is seconds. Plan your CI timeouts accordingly.
Quiz: Quiz
Loading practice…