Test Database Code with Real Postgres
Use Testcontainers, Drizzle, and Vitest to test queries against real Postgres.
Introduction
30 Second Summary
A green test result feels reassuring until the real app still fails to save or find a user. That gap can stay hidden until the software meets real data.
In this project, you will build a Node test suite that proves your Drizzle data access code works against a real Postgres database. Testcontainers gives each Vitest run a temporary Postgres database inside Docker.
What You'll Build
You will run the suite to see a user record complete a real database round trip, then watch one broken filter turn the passing run into a clear failure.
By the end of this project, you'll have:
- A working database round trip that saves a user. Your own query returns the same record.
- A disposable test database that appears in Docker while the suite runs. It disappears after the tests finish.
- A deliberate query failure that proves the suite can catch a real data access bug before you restore the passing result.
- Secret Mission: Add a real database test that updates a user's details. Confirm that your query returns the saved changes.
Are there any prerequisites?
You need Node, a running Docker environment, and a code editor. No cloud account is required.
Before We Start
Mocked database tests can pass even when a real database rejects the query. This project closes that gap by giving your tests a real database they can use safely.
To make that possible, Docker must be able to start a disposable container on your machine. Node.js provides the runtime for the project.
The npm package manager installs the project tools later. A terminal plus a code editor gives you somewhere to run commands and work with the files.
In this step, get ready to:
- Install Docker for local containers.
- Prepare Node.js and npm for project tooling.
- Run a disposable Docker test container.
Install and start Docker
Docker's command-line tool sends requests to a background daemon. A healthy version check shows that both pieces can communicate.
- Press Cmd+Space on macOS or the Windows key on Windows to open operating system search.
- Type Terminal on macOS or PowerShell on Windows into the search field.
- Press Enter to open the terminal.
- Check whether Docker and its daemon are ready by running this command:
docker version
What Does This Check?
The command asks the Docker client for version information. It also asks the daemon for its version information.
Seeing details from both sides proves that the installed command-line tool can reach the running daemon.
✔️ I see client and server details
Strong start: Docker's command-line tool can reach the daemon. Your machine is ready for the container test later in this step.
ⓧ I only see client details
Docker is installed. Its daemon still needs to start before it can run containers.
- Press Cmd+Space on macOS or the Windows key on Windows to open operating system search.
- Type Docker Desktop into the search field.
- Press Enter to start Docker Desktop.
- Select Accept if the subscription agreement appears.
- Wait for Docker Desktop to finish starting.
- Check the daemon again by running this command:
docker version
What Confirms the Daemon Is Running?
The output now contains information from the command-line client plus the Docker engine. That second set of details proves the daemon answered your request.
ⓧ Docker command not found
Docker is missing from your machine or terminal path. Install Docker Desktop using the instructions for your operating system.
macOS
- Open Docker's macOS installation guide in your browser.
- Download the installer that matches your Mac chip.
- Double-click Docker.dmg to start the installer.
- Drag the Docker icon into the Applications folder.
Docker Desktop is now installed. Its first launch starts the daemon that handles container requests.
- Press Cmd+Space to open Spotlight.
- Type Docker Desktop into the search field.
- Press Enter to start Docker Desktop.
- Select Accept if the subscription agreement appears.
- Wait for Docker Desktop to finish starting.
Windows
- Open Docker's Windows installation guide in your browser.
- Download Docker Desktop Installer.exe.
- Double-click Docker Desktop Installer.exe to start the installer.
- Choose the per-user installation mode when prompted.
- Complete the installation wizard.
The application is installed. Starting it launches the daemon that receives Docker commands.
- Press the Windows key to open search.
- Type Docker Desktop into the search field.
- Press Enter to start Docker Desktop.
- Select Accept if the subscription agreement appears.
- Wait for Docker Desktop to finish starting.
Linux
- Open Docker's installation guide in your browser.
- Select the installation path for your Linux distribution.
- Complete the package installation steps from the selected guide.
- Start Docker using the service instructions from the same guide.
- Return to the terminal from your earlier check.
Prepare Node.js and a code editor
The project code runs with Node.js. The npm package manager installs the testing plus database libraries used in later steps.
- Verify both tools in the terminal from your Docker check by running these commands:
node --version
npm --version
What Do These Checks Prove?
The first command confirms that your terminal can find the Node.js runtime. The second confirms that npm was installed alongside it.
Two version numbers mean your JavaScript toolchain is ready for the project.
✔️ I see two version numbers
Your JavaScript toolchain is ready. Node.js can run the project while npm manages its packages.
ⓧ A command is not found
Installing the current Long Term Support release gives you Node.js plus npm in one setup process.
- Open the official Node.js download page in your browser.
- Select the LTS installer for your operating system.
- Run the downloaded installer.
- Complete the installation process.
- Close the terminal from your earlier check.
- Reopen your terminal through operating system search.
- Verify the installation by running these commands:
node --version
npm --version
What Should You See Now?
Each command prints a version number. This confirms that the new terminal session can find Node.js plus npm.
A code editor gives you a focused place to create the schema plus tests in the next steps. Any editor that can work with JavaScript files is suitable.
- Choose the code editor you want to use for this project.
- Install it from its official website if your computer does not have it.
- Press Cmd+Space on macOS or the Windows key on Windows to open operating system search.
- Type the name of your code editor into the search field.
- Press Enter to launch the editor.
You should see your editor open to its starting screen. Your terminal plus editing tools are now available.
Run a disposable test container
A small test image checks the full path from your terminal to Docker's daemon. The container removes itself after it exits.
Before you run this, do you think Docker will need to download the test image first?
- Test image pulling plus container execution by running this command:
docker run --rm hello-world
What Did This Prove?
A success message proves Docker can pull an image. It also proves the daemon can run a container.
The --rm flag removes the stopped container automatically. Your machine returns to a clean state after the check.
Docker may download the image before the container starts. You will then see a confirmation that the installation works.
That successful round trip gives your future database tests a working container runtime.
Container Test Not Completing?
- Switch back to Docker Desktop if the application closed.
- Wait for Docker to finish starting before you rerun the test.
- Check your network connection if the image download stalls.
Help me diagnose why my Docker test container cannot start.
Your Docker daemon is running. It has successfully run a disposable test container from the terminal.
Your local runtime tools are ready. Next, you will create the Node project and define its database schema.
Create the Drizzle Project
Your Docker check proved that the runtime can launch containers. The test suite still needs a Node.js project that describes the Postgres data it will exercise.
This step creates that project with Drizzle ORM as the schema layer. You will add Vitest plus Testcontainers so later steps can run the schema against a disposable database.
In this step, get ready to:
- Create a Node.js project named real-postgres-drizzle-tests.
- Install the database packages plus the test tools.
- Define the users table in src/schema.ts.
Initialize the Node.js project
A package.json file gives npm a record of the project name plus its packages. Starting on your Desktop keeps the project folder easy to find.
- Switch back to the terminal from earlier.
- Move to your Desktop by running this command:
cd ~/Desktop
Why start on your Desktop?
This command changes the terminal location to your Desktop. The project folder you create next appears somewhere you can find without hunting through system folders.
- Create the real-postgres-drizzle-tests folder plus its initial package.json file by running these commands:
mkdir real-postgres-drizzle-tests
cd real-postgres-drizzle-tests
npm init -y
What did npm create?
- The first command creates the project folder on your Desktop.
- The second command moves the terminal into that folder.
- The final command creates package.json with default project details.
You will see npm print the generated project details. The displayed file path ends with real-postgres-drizzle-tests/package.json.
Project folder not created?
Check that the terminal prompt shows your Desktop before creating the folder. Remove any partial folder before repeating the commands.
If the terminal still stops, ask for help with the exact output you can see. Help me understand why my Node project folder or package.json file was not created.
Install the test stack
The project needs runtime packages for database queries. It also needs development packages for schema tooling plus automated tests.
The first install may sit quietly for a minute while npm downloads the packages. That pause is normal.
- Install the database packages plus the test tools by running these commands:
npm i drizzle-orm pg
npm i -D drizzle-kit
npm install -D vitest
npm install testcontainers --save-dev
What does each package do?
- The drizzle-orm package maps TypeScript definitions to database tables.
- The pg package provides the PostgreSQL driver.
- The drizzle-kit package applies schema definitions to a database.
- The vitest package runs the automated test suite.
- The testcontainers package controls disposable containers from the test suite.
- Confirm that npm recorded the installed packages by running this command:
npm pkg get dependencies devDependencies
What should you see?
You will see dependency entries for drizzle-orm, pg, drizzle-kit, vitest, and testcontainers. This confirms that npm saved the full stack in package.json.
Package installation stopped?
Check that your terminal is still inside real-postgres-drizzle-tests. Confirm that your internet connection can reach the npm registry before retrying the failed command.
Share the visible installation output without including any credentials. Help me diagnose why these npm packages did not install.
An npm test script gives the project one repeatable command for every test run. Using vitest run makes each run finish after reporting the result.
- Set the test script in package.json by running this command:
npm pkg set scripts.test="vitest run"
What does this command change?
The command updates the scripts.test field inside package.json. Later steps can run the complete suite through npm.
- Check the saved test script by running this command:
npm pkg get scripts.test
What should the script show?
You will see vitest run as the saved value. The project now has a test entry point ready for the suite you build later.
Test script looks different?
Repeat the set command from inside real-postgres-drizzle-tests. A command run from another folder updates a different package.json file.
If the value still differs, ask for help with your current folder path plus the displayed script. Help me correct the test script in my package.json file.
Define the users schema
The schema becomes the contract between the data access code and Postgres. It names each column plus the rule that keeps email values unique.
- Switch back to the code editor from earlier.
- Use the editor's folder picker to select real-postgres-drizzle-tests from your Desktop.
You will see real-postgres-drizzle-tests at the top of the editor's file sidebar. The folder contains package.json plus the installed package files.
- Create a folder named src inside real-postgres-drizzle-tests.
You will see src appear beneath the project name in the file sidebar.
- Create schema.ts inside the src folder.
- Paste the following schema into src/schema.ts:
import { integer, pgTable, text } from "drizzle-orm/pg-core";
// Describe the PostgreSQL shape used by the data access tests.
export const users = pgTable("users", {
id: integer("id").primaryKey(),
name: text("name"),
email: text("email").unique(),
});
What does this schema define?
- The pgTable() call defines a Postgres table named users.
- The id column stores integers plus a primary key rule.
- The name column stores text.
- The email column stores text plus a unique constraint.
- Save src/schema.ts.
Before you review the file, which column do you expect to reject duplicate values?
- Review the saved src/schema.ts file in your editor.
You will see the exported users table with the id, name, and email columns. The id column has primaryKey() while the email column has unique().
That is a solid foundation: your data access tests now have an exact table contract to exercise.
Schema showing an import problem?
Confirm that src/schema.ts sits inside the project you opened. Check that drizzle-orm appears under dependencies in package.json.
Compare every quote plus bracket with the reference below. Help me find the mismatch in my Drizzle users schema.
✔️ Awesome, I've got everything!
Your saved schema matches the finished src/schema.ts file.
ⓧ I'd like to double check the full code
import { integer, pgTable, text } from "drizzle-orm/pg-core";
// Describe the PostgreSQL shape used by the data access tests.
export const users = pgTable("users", {
id: integer("id").primaryKey(),
name: text("name"),
email: text("email").unique(),
});
Your Node project now has its test stack plus a Drizzle schema. Next up, you will connect the suite to a disposable Postgres container.
Start Postgres with Testcontainers
Your Drizzle schema now describes the users table. The missing piece is a real database that exists only for the test run.
In this step, Vitest uses Testcontainers to launch PostgreSQL. Docker hosts the temporary database.
The suite receives the database connection through DATABASE_URL. Teardown removes the container when the suite ends.
In this step, get ready to:
- Create a global setup that manages a disposable Postgres container.
- Configure Vitest to load the setup before the suite.
- Watch the temporary container appear during the test run.
Create the global setup file
A global setup runs once before the test workers begin. Its matching teardown runs after every test file finishes.
- Create a test folder inside the real-postgres-drizzle-tests folder using your editor's file controls.
- Create setup.ts inside the new test folder.
You should now see test/setup.ts in the editor's file tree.
- Add the container lifecycle to test/setup.ts by pasting this code:
import { PostgreSqlContainer } from "@testcontainers/postgresql";
// Start one PostgreSQL container for the whole test run.
export default async function setup() {
const container = await new PostgreSqlContainer("postgres:16-alpine").start();
// Give the test process the container's mapped connection URL.
process.env.DATABASE_URL = container.getConnectionUri();
// Remove the temporary database after the suite finishes.
return async function teardown() {
await container.stop();
};
}
What does this code do?
- PostgreSqlContainer starts the official postgres:16-alpine image with temporary database credentials.
- getConnectionUri() reads the mapped host port from the running container.
- DATABASE_URL gives the test process the complete connection string.
- teardown() stops the container after the suite finishes.
- Save test/setup.ts.
- Inspect test/setup.ts for TypeScript error underlines.
You should see no TypeScript errors in the saved file.
Seeing an import error?
- Check that package.json lists @testcontainers/postgresql.
- Confirm the class name is spelled PostgreSqlContainer.
- Confirm the import begins with @testcontainers/postgresql.
Help me fix the Testcontainers import in my Vitest global setup.
✔️ Awesome, I've got everything!
Great. Your saved setup now owns the Postgres container lifecycle.
ⓧ I'd like to double check the full code
import { PostgreSqlContainer } from "@testcontainers/postgresql";
// Start one PostgreSQL container for the whole test run.
export default async function setup() {
const container = await new PostgreSqlContainer("postgres:16-alpine").start();
// Give the test process the container's mapped connection URL.
process.env.DATABASE_URL = container.getConnectionUri();
// Remove the temporary database after the suite finishes.
return async function teardown() {
await container.stop();
};
}
Connect the setup to Vitest
Vitest needs the setup file path before it can run the container lifecycle. The globalSetup option loads that file once around the entire suite.
- Create vitest.config.ts inside the top level of the real-postgres-drizzle-tests folder using your editor's file controls.
- Configure Vitest to load test/setup.ts by pasting this code:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// Run the container lifecycle once around the suite.
globalSetup: "./test/setup.ts",
},
});
What does this configuration do?
- defineConfig() gives the file Vitest-aware configuration types.
- globalSetup points Vitest to the file that starts the database.
- ./test/setup.ts resolves from the real-postgres-drizzle-tests folder.
- Save vitest.config.ts.
- Inspect vitest.config.ts for TypeScript error underlines.
You should see a saved configuration with no TypeScript errors.
Setup file not loading?
- Confirm the configuration key is globalSetup.
- Check that the configured path is exactly ./test/setup.ts.
- Confirm vitest.config.ts sits inside real-postgres-drizzle-tests.
Help me find why Vitest is skipping my global setup.
✔️ Awesome, I've got everything!
Your Vitest configuration now points to the container setup file.
ⓧ I'd like to double check the full code
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// Run the container lifecycle once around the suite.
globalSetup: "./test/setup.ts",
},
});
Watch the container lifecycle
The strongest check is the container itself. It should exist while Vitest is active.
Before you start the suite, do you think the temporary database will remain after Vitest stops?
- Start Vitest in watch mode from the terminal inside real-postgres-drizzle-tests by running this command:
npx vitest
What happens during this run?
- Vitest loads test/setup.ts before it creates the test workers.
- Testcontainers asks Docker to start the Postgres image.
- The mapped connection URL becomes DATABASE_URL for the suite.
The first run can take longer while Docker downloads the Postgres image. A quiet terminal during that download does not mean the setup has stalled.
- Switch back to Docker Desktop from earlier.
- Check the running containers list for the active database.
You should see a temporary container using the postgres:16-alpine image while Vitest remains active.
- Return to the terminal from earlier.
- Press Ctrl+C to stop Vitest.
- Switch back to Docker Desktop from earlier.
- Check the running containers list again.
You should no longer see the temporary Postgres container. The teardown removed it after Vitest stopped.
Container missing or left running?
- Confirm Docker is still running before starting Vitest again.
- Check the terminal for an error mentioning the container startup.
- Confirm teardown() still calls container.stop().
Help me debug the Postgres container lifecycle.
You have the key proof: your test suite can now summon a real Postgres database and remove it afterwards.
Next, you will push the Drizzle schema into that database so the suite can test real inserts and queries.
Test Real Database Queries
Your Testcontainers suite now proves Docker can start a temporary Postgres database. The database still has no tables for your data access code to use.
This step closes that gap with Drizzle ORM. The suite will push your schema before Vitest runs.
In this step, get ready to:
- Push the users schema into each temporary database.
- Connect data access functions to the temporary database.
- Prove insertion plus email lookup with real database round trips.
Push the schema into the temporary database
Each test container begins as an empty Postgres database. Drizzle Kit must create the `users` table after DATABASE_URL points to that container.
- In test/setup.ts, find the line that assigns the container connection URL to process.env.DATABASE_URL.
- Add this schema push directly below that assignment:
// Push the schema after Testcontainers exposes the database URL
const { execSync } = await import('node:child_process')
execSync('npx drizzle-kit push --dialect=postgresql --schema=./src/schema.ts', {
env: process.env,
stdio: 'inherit',
})
What does this setup code do?
- The dynamic import loads execSync() only when the setup reaches the schema push.
- The inherited process.env passes the temporary connection URL to Drizzle Kit.
- The synchronous process pauses setup until the users table exists.
- Save test/setup.ts.
Before you run the suite, which happens first: the schema push or the tests? Hold that prediction.
- Run the existing test suite from the real-postgres-drizzle-tests terminal by running this command:
npm test
What happens during this run?
- The test script asks Testcontainers to start a fresh Postgres container.
- The setup exposes the container URL through DATABASE_URL.
- Drizzle Kit applies src/schema.ts before Vitest begins its checks.
You should see Drizzle Kit report a successful schema push before the existing suite completes. This proves each test run receives a usable users table.
Schema push failing?
- Check that Docker is still running before you start the suite.
- Confirm the schema path in the command is exactly ./src/schema.ts.
- Confirm the schema push remains below the DATABASE_URL assignment.
Help me diagnose why Drizzle Kit cannot push my schema into the Testcontainers database.
Build the database access layer
A shared database client keeps every query attached to the URL created by the test container. The node-postgres driver gives Drizzle that connection.
- Create src/db.ts inside the src folder using your editor's file creation control.
- Add the database client by pasting this code:
import { drizzle } from 'drizzle-orm/node-postgres'
// Connect every query to the URL created by the test container
export const db = drizzle(process.env.DATABASE_URL!)
What does this client do?
- The Drizzle client reads DATABASE_URL when the test file imports db.
- The setup file has already assigned that URL to the running test container.
- Every query using db therefore reaches the temporary Postgres database.
- Save src/db.ts.
- Check src/db.ts in the editor for error markers.
You should see no new TypeScript errors in src/db.ts.
Database client showing an error?
- Check that the import path is exactly drizzle-orm/node-postgres.
- Confirm the exported client is named db.
Help me fix the TypeScript error in my Drizzle database client.
✔️ Awesome, I've got everything!
Your shared Drizzle client now targets the database URL created for each test run.
ⓧ I'd like to double check the full code
import { drizzle } from 'drizzle-orm/node-postgres'
// Connect every query to the URL created by the test container
export const db = drizzle(process.env.DATABASE_URL!)
What should match?
Your file should import the node-postgres Drizzle driver. It should export one client named db.
The client supplies the connection. Your data access functions now define the real insert plus lookup behavior that the tests will exercise.
- Create src/users.ts inside the src folder using your editor's file creation control.
- Add the user data access functions by pasting this code:
import { eq } from 'drizzle-orm'
import { db } from './db'
import { users } from './schema'
// Insert one user and return the row created by Postgres
export async function insertUser(user: typeof users.$inferInsert) {
const [createdUser] = await db.insert(users).values(user).returning()
return createdUser
}
// Find the row whose email matches the requested address
export async function findUserByEmail(email: string) {
const [user] = await db.select().from(users).where(eq(users.email, email))
return user
}
How do these functions work?
- The inferred insert type keeps each user object aligned with the existing Drizzle schema.
- The returning() call gives the test the row created by Postgres.
- The eq() filter compares the supplied email with users.email.
- Save src/users.ts.
- Check src/users.ts in the editor for error markers.
You should see no new TypeScript errors in src/users.ts.
User functions showing errors?
- Confirm users matches the table export in src/schema.ts.
- Check that both relative imports begin with ./.
- Confirm the email filter uses users.email.
Help me debug the Drizzle insert or email lookup functions in src/users.ts.
✔️ Awesome, I've got everything!
Your data access layer can now insert a user or find one by email.
ⓧ I'd like to double check the full code
import { eq } from 'drizzle-orm'
import { db } from './db'
import { users } from './schema'
// Insert one user and return the row created by Postgres
export async function insertUser(user: typeof users.$inferInsert) {
const [createdUser] = await db.insert(users).values(user).returning()
return createdUser
}
// Find the row whose email matches the requested address
export async function findUserByEmail(email: string) {
const [user] = await db.select().from(users).where(eq(users.email, email))
return user
}
What should match?
Your file should export insertUser() plus findUserByEmail(). The lookup must filter on users.email.
Prove the round trips with Vitest
A real database test checks the row that Postgres stores. The next file proves the insert result plus the email lookup without replacing the database with a mock.
- Create test/users.test.ts inside the test folder using your editor's file creation control.
- Add the two round-trip tests by pasting this code:
import { expect, test } from 'vitest'
import { findUserByEmail, insertUser } from '../src/users'
test('inserts a user', async () => {
// Write a complete row to the temporary Postgres database
const insertedUser = await insertUser({
name: 'Ada Lovelace',
email: 'ada@example.com',
})
expect(insertedUser).toMatchObject({
name: 'Ada Lovelace',
email: 'ada@example.com',
})
})
test('finds a user by email', async () => {
// Read the same row through the production lookup function
const foundUser = await findUserByEmail('ada@example.com')
expect(foundUser).toMatchObject({
name: 'Ada Lovelace',
email: 'ada@example.com',
})
})
What do these tests prove?
- The first test sends a real insert through insertUser().
- Its assertion checks the values returned by Postgres.
- The second test uses findUserByEmail() to retrieve the stored row.
- Its assertion proves the email filter reaches the expected record.
✔️ Awesome, I've got everything!
Your test file now checks both real database round trips.
ⓧ I'd like to double check the full code
import { expect, test } from 'vitest'
import { findUserByEmail, insertUser } from '../src/users'
test('inserts a user', async () => {
// Write a complete row to the temporary Postgres database
const insertedUser = await insertUser({
name: 'Ada Lovelace',
email: 'ada@example.com',
})
expect(insertedUser).toMatchObject({
name: 'Ada Lovelace',
email: 'ada@example.com',
})
})
test('finds a user by email', async () => {
// Read the same row through the production lookup function
const foundUser = await findUserByEmail('ada@example.com')
expect(foundUser).toMatchObject({
name: 'Ada Lovelace',
email: 'ada@example.com',
})
})
What should match?
Your file should contain one insertion test plus one email lookup test. Both assertions should expect the same name plus email values.
- Save test/users.test.ts.
Before the final run, do you expect both data access functions to survive a real database round trip? Lock in your prediction.
- Run the complete test suite against a fresh temporary Postgres database by running this command:
npm test
What does this run prove?
- Testcontainers starts a fresh Postgres database for the suite.
- Drizzle Kit creates the users table from your schema.
- Vitest exercises the same functions your application code would call.
- The setup removes the temporary container after the suite finishes.
You should see both user tests pass. That is the payoff: both data access functions just completed real round trips through a temporary Postgres database.
- Switch back to the Docker container view from earlier.
- Confirm the temporary Postgres container from the test run is no longer listed.
Database tests not passing?
- Check that the insertion test runs before the email lookup test in test/users.test.ts.
- Confirm both tests use the same email value.
- Check that findUserByEmail() filters on users.email.
Help me debug my failing real Postgres tests.
Both round trips are green against a real database. Next, you will break the email query on purpose to see whether this suite catches the defect.
Break the Query
The previous step proved your insert reaches a real Postgres database. It also proved your lookup returns the saved user.
A useful test suite must also expose defects. In this step, you deliberately point the lookup at a missing column before Vitest sends it through Drizzle to the disposable Testcontainers database.
In this step, get ready to:
- Redirect the find-user query to a missing database column.
- Run the real database suite against the broken query.
- Inspect what PostgreSQL reports.
Point the query at a missing column
Drizzle's schema-backed columns protect typed queries from column-name mistakes. Its raw SQL template can pass a column name outside the schema, which creates the controlled defect for this test.
- In the code editor from earlier, select src/users.ts from the Explorer sidebar.
- Find the current import from drizzle-orm.
- Replace the current filter-helper import with this line:
import { sql } from 'drizzle-orm';
Why Use the SQL Helper?
- The sql helper lets the query include a raw SQL fragment.
- The helper still keeps dynamic query values parameterized.
- Find the .where() call that currently compares the email argument with users.email.
- Replace that filter with this line:
.where(sql`username = ${email}`);
What Does This Line Change?
- The function still receives the existing email argument.
- The filter now names username directly instead of using users.email from the schema.
- The username column is absent from src/schema.ts.
- Save src/users.ts.
Run the broken query
Before you run the suite, which layer do you think will expose the bad column first?
- Switch back to the terminal panel from the previous step.
- Run the broken query against the disposable PostgreSQL database by using:
npm test
What Does This Command Check?
- The command launches the configured Vitest suite.
- Testcontainers starts a disposable PostgreSQL database.
- The setup pushes the users schema before the tests execute.
- The retrieval test sends the broken query to the running database.
You will see the retrieval test fail. The database error explains that the username column does not exist.
That is the result you were looking for. Your suite has caught a real database defect before it escaped into the application.
Why This Failure Matters
PostgreSQL checks the query against the schema that actually exists inside the container. The failure proves your test exercises the real database boundary.
A mock can reproduce behavior you expect. This container exposes behavior the database enforces.
Does the suite still pass?
- Confirm that you saved src/users.ts.
- Confirm that the .where() call names the raw username column.
- Confirm that the filter no longer references users.email.
Help me work out why the deliberately broken database query still passes.
Your real database test caught the defect. Next, you will restore the correct column and prove the suite returns to green.
Fix and Verify the Suite
The deliberate failure has done its job. Your real Postgres test caught a query that disagreed with the schema.
The fix reconnects the lookup to the email column defined by Drizzle ORM. Vitest can then exercise the corrected query through Testcontainers.
The last check confirms that the temporary database disappears after the suite completes.
In this step, get ready to:
- Restore the email filter in src/users.ts.
- Rerun the insert test and the lookup test against real Postgres.
- Confirm Docker has no project Postgres container running.
Restore the email filter
The schema maps users.email to the real email column. The lookup must use that mapped column to match the test input.
- In the code editor's file sidebar, select src/users.ts.
- Find the query filter that references the nonexistent username database column.
- Replace the column reference with users.email.
- Save src/users.ts.
Why does this fix the failure?
The test passes an email address into the lookup. Using users.email generates a query against the column that exists in the temporary database.
Rerun the real database tests
Before you run the suite, do you expect the corrected filter to make both tests pass?
- Return to the terminal where the failing suite ran.
- Run the test script from the real-postgres-drizzle-tests folder by using this command:
npm test
What does this command prove?
- The test script launches Vitest.
- Vitest starts a temporary Postgres container through Testcontainers.
- The setup pushes the Drizzle schema before the tests execute.
- The suite removes the container after the tests finish.
You'll see the insert test pass. You'll also see the email lookup test pass.
That is the proof you were aiming for: both tests now pass against real Postgres.
Are the tests still failing?
- Confirm Docker is still running if the suite cannot start its temporary container.
- Check that src/users.ts is saved with users.email in the lookup filter.
- Check that no other reference to username remains in the lookup query.
Help me diagnose why my real Postgres Vitest suite still fails after changing the filter to users.email.
Confirm the container was removed
Passing tests cover the query behavior. The suite also owns the database teardown.
Before you check, do you expect the project Postgres container to remain in Docker's running list?
- List the containers that are still running by using this command:
docker ps
What does this command show?
This command lists only containers that are currently running. A removed test container stays out of the results.
You should see no Postgres container associated with real-postgres-drizzle-tests. Any unrelated running containers can remain in the list.
Your lifecycle is complete: Testcontainers created the database for the suite. It also removed the database after the suite.
Still see the project container?
- Confirm the test process has fully exited.
- Repeat the container check after the terminal returns to its prompt.
Help me investigate why the temporary Postgres container is still running.
Your repository now has a passing real-database suite that cleans up its Postgres container after every completed run.
Secret mission
Prove Your Tests Are Repeatable
Run your real database suite in two separate processes. Prove that each run gets a fresh database before Testcontainers removes the final container.
Clean Up Your Resources
Clean Up Your Resources
Everything in this project runs locally with no ongoing costs. You can keep the real-postgres-drizzle-tests folder, pause Docker, or delete the folder.
Resources you used:
- The local real-postgres-drizzle-tests folder.
Why Isn't the Database Listed?
Your automatic cleanup already worked. Testcontainers stopped the temporary PostgreSQL container after the test suite finished.
No project database container remains in Docker. The local project folder is the only resource left for you to manage.
Keep everything running
No action needed. Choose this if you want to reuse the real database testing setup in another project.
- Keep the real-postgres-drizzle-tests folder on your computer.
- Return to Docker from earlier.
- Confirm its container list shows no project PostgreSQL container.
Pause - I'll come back to this later
Shut down Docker to free up local memory while keeping your project files. The temporary database container is already gone.
- Stop Docker with the shutdown control in your installed runtime.
- Keep the real-postgres-drizzle-tests folder on your computer.
- Restart Docker before running the test suite again.
Delete - I don't want to use this again
Remove the local project folder if you do not want to reuse this testing setup.
Save Anything You Need First
Deleting the folder is permanent. Copy any code you want to keep before continuing.
Docker stays installed. There is no project PostgreSQL container to delete.
macOS
- Return to the code editor from earlier.
- Open its integrated terminal for the real-postgres-drizzle-tests folder.
- Remove the project folder by running these commands:
cd ..
rm -rf real-postgres-drizzle-tests
ls
What Do These Commands Do?
- The cd command moves the terminal to the folder containing your project folder.
- The rm command permanently removes the project folder with its contents.
- The ls command lists the remaining items for verification.
You should no longer see real-postgres-drizzle-tests in the output.
Still See the Project Folder?
Check that the terminal started inside real-postgres-drizzle-tests before the first command moved up one folder. Close any process that still uses a file inside the project before trying again.
Help me diagnose the cleanup problem.
Windows
- Return to the code editor from earlier.
- Open an integrated terminal for the real-postgres-drizzle-tests folder.
- Select Command Prompt as the terminal shell.
- Remove the project folder by running these commands:
cd ..
rmdir /s /q real-postgres-drizzle-tests
dir
What Do These Commands Do?
- The cd command moves the terminal to the folder containing your project folder.
- The rmdir command permanently removes the project folder with its contents.
- The dir command lists the remaining items for verification.
You should no longer see real-postgres-drizzle-tests in the output.
Still See the Project Folder?
Close the project in your code editor if Windows reports that a file is in use. Start a new Command Prompt terminal in the folder that previously contained real-postgres-drizzle-tests before trying again.
Help me diagnose the cleanup problem.
Linux
- Return to the code editor from earlier.
- Open its integrated terminal for the real-postgres-drizzle-tests folder.
- Remove the project folder by running these commands:
cd ..
rm -rf real-postgres-drizzle-tests
ls
What Do These Commands Do?
- The cd command moves the terminal to the folder containing your project folder.
- The rm command permanently removes the project folder with its contents.
- The ls command lists the remaining items for verification.
You should no longer see real-postgres-drizzle-tests in the output.
Still See the Project Folder?
Check that the terminal started inside real-postgres-drizzle-tests before the first command moved up one folder. Close any process that still uses a file inside the project before trying again.
Help me diagnose the cleanup problem.
Nice Work!
Nice Work!
Great job! You built a Vitest suite that checks your data access code against a real temporary Postgres database. Your workflow now spans Testcontainers, Docker, and Drizzle ORM from container startup through cleanup.
You've learned how to:
- Ran a disposable Postgres database with Docker. Confirmed that Testcontainers removes the container after the suite.
- Created a Drizzle users schema. Proved that inserts plus email queries round-trip against the real database.
- Broke the user lookup query on purpose. Watched Vitest catch the failure. Restored the email filter to get a passing suite.
- Secret Mission: Extended the finished repository through an optional challenge that pushes your real-database testing skills further.
Ready to quiz yourself?