第18章:DBありの統合テスト(Composeで起動)🧱🧪
この章では「本物のDB(Postgres)を、Docker Composeで起動して」「Vitestで統合テストを回す」ところまで作ります😊 モックじゃ拾えない 制約(UNIQUE/NOT NULL)・SQLの挙動・実DB接続の落とし穴を、ちゃんとテストできるようにするのがゴールです🔥
1) まず“統合テスト”って何を守るの?🧠✨
ユニットテストが「関数の正しさ」を守るなら、DBあり統合テストはこういうのを守ります👇
- ✅ SQLが正しい(JOIN/WHERE/ORDERのミスを拾う)
- ✅ 制約が効いてる(UNIQUE違反、外部キー、NULL禁止など)
- ✅ 実際の接続情報で動く(接続先ホスト名・認証・タイムアウト)
- ✅ マイグレーション/初期化が成立してる(起動したらスキーマがある)
そしてComposeで起動すると「DBが必要な時だけ」「同じ手順で」「CIでもそのまま」が作れます💪
2) 今回の“正解ルート”🧭(初心者でも事故りにくい)
ここは超大事。Composeは 起動順は見てくれるけど、準備完了までは待たない んです😇 なので healthcheck + depends_on(condition: service_healthy) を入れて「DBが起きるまで待つ」を作ります。(Docker Documentation)
さらに、DB初期化(/docker-entrypoint-initdb.d)は “空のデータディレクトリ”の時だけ実行されます。(Docker Hub)
→ だからテストでは **匿名ボリューム + docker compose up -V(匿名ボリューム再生成)**が相性いいです👍(Docker Documentation)
3) Composeを“テスト用プロファイル”に分離する🧩
「普段の開発ではDBテスト用コンテナはいらない」ので、Composeの profiles を使います。(Docker Documentation)
テスト時だけ --profile test で起動できるようにします😊
4) 実装:compose.yaml(テスト用DB + テスト実行サービス)🛠️
プロジェクト直下に compose.yaml がある想定で、こうします👇
services:
db:
image: postgres:18
profiles: ["test"]
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: app_test
# DBが本当に“接続可能”になるまで待てるようにする
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 20
start_period: 5s
volumes:
# 初期SQL(空のDBのときだけ実行される)
- ./db/init:/docker-entrypoint-initdb.d:ro
# 匿名ボリューム(-V で毎回作り直せる)
- /var/lib/postgresql/data
test:
profiles: ["test"]
build:
context: .
environment:
DATABASE_URL: postgres://test:test@db:5432/app_test
depends_on:
db:
condition: service_healthy
command: ["npm", "run", "test:integration"]
ポイント👀
profiles: ["test"]→ テスト時だけ起動(普段は無視される)(Docker Documentation)depends_on: condition: service_healthy→ DBのhealthcheck成功まで待つ(Docker Documentation)./db/init→ 初期スキーマを入れる(ただし空DBのときだけ)(Docker Hub)DATABASE_URLのホストはdb(Composeのサービス名)✨
5) DB初期化SQLを置く🧱📄
db/init/001_schema.sql を作ります👇
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL
);
UNIQUE制約があるので、「重複メール」を入れた時にちゃんと落ちるかがテストできます😊
6) Node側:最小のDBアクセス(pg)🐘
pg を使う例にします(軽くて分かりやすい)✨
6-1) 依存追加
npm i pg
npm i -D @types/pg
6-2) src/db.ts
import { Pool } from 'pg'
const connectionString = process.env.DATABASE_URL
if (!connectionString) throw new Error('DATABASE_URL is missing')
export const pool = new Pool({ connectionString })
6-3) src/userRepo.ts
import { pool } from './db'
export async function createUser(email: string, name: string) {
const result = await pool.query(
'INSERT INTO users(email, name) VALUES($1, $2) RETURNING id, email, name',
[email, name],
)
return result.rows[0] as { id: number; email: string; name: string }
}
export async function findUserByEmail(email: string) {
const result = await pool.query('SELECT id, email, name FROM users WHERE email = $1', [email])
return (result.rows[0] ?? null) as null | { id: number; email: string; name: string }
}
export async function truncateAll() {
await pool.query('TRUNCATE TABLE users RESTART IDENTITY CASCADE')
}
7) Vitest:統合テスト用の設定を分ける🧪⚙️
統合テストは 遅いし、並列で壊れやすいので「別設定」にするのが勝ちです😊 Vitestは **projects(旧workspace相当)**で “設定違いのテスト” を同居できます。(Vitest)
7-1) vitest.integration.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['test/integration/**/*.test.ts'],
testTimeout: 30_000,
hookTimeout: 30_000,
maxWorkers: 1, // DB共有するならまず1が安全👍
},
})
7-2) package.json に追加
{
"scripts": {
"test:integration": "vitest -c vitest.integration.config.ts run"
}
}
8) 統合テストを書く(DBを本当に叩く)🧪🔥
test/integration/users.test.ts
import { beforeAll, beforeEach, afterAll, test, expect } from 'vitest'
import { pool } from '../../src/db'
import { createUser, findUserByEmail, truncateAll } from '../../src/userRepo'
beforeAll(async () => {
// ここで接続できる=Composeの起動待ちが効いてる👍
await pool.query('SELECT 1')
})
beforeEach(async () => {
await truncateAll()
})
afterAll(async () => {
await pool.end()
})
test('ユーザーを作って検索できる', async () => {
await createUser('a@example.com', 'Alice')
const user = await findUserByEmail('a@example.com')
expect(user?.name).toBe('Alice')
})
test('メール重複はUNIQUE制約で落ちる', async () => {
await createUser('dup@example.com', 'A')
await expect(() => createUser('dup@example.com', 'B')).rejects.toThrow()
})
Vitestの beforeAll/beforeEach/afterAll はこういう用途にピッタリです👌
重い処理(seedやサーバ起動)は “setupFilesよりglobal setup/ beforeAll” が向いてるよ、という話も公式で触れられてます。(Vitest)
9) いよいよ「Composeで起動してテスト」🚀
9-1) まずは手動で動作確認(2コマンド)
VS Codeのターミナルで👇
docker compose --profile test up --build -V --abort-on-container-exit --exit-code-from test
--exit-code-from testは「testサービスの終了コードを返す」オプションで、同時に--abort-on-container-exitも効きます。(Docker Documentation)-Vは匿名ボリュームを作り直すので、DB初期化SQLが毎回ちゃんと走りやすくなります👍(Docker Documentation)
終わったら後片付け👇
docker compose --profile test down --volumes --remove-orphans
9-2) “ワンコマンド化”する(失敗してもdownする)🧰✨
Windowsだと && だけだと失敗時に掃除が飛びがちなので、Nodeスクリプトで安全にまとめます🙂
scripts/test-it.mjs
import { spawnSync } from 'node:child_process'
function run(cmd, args, { allowFail = false } = {}) {
const r = spawnSync(cmd, args, { stdio: 'inherit' })
if (!allowFail && r.status !== 0) {
process.exitCode = r.status ?? 1
throw new Error(`${cmd} failed`)
}
return r.status ?? 0
}
let exitCode = 0
try {
exitCode = run('docker', [
'compose', '--profile', 'test',
'up', '--build', '-V',
'--abort-on-container-exit',
'--exit-code-from', 'test',
])
} catch {
// exitCodeは process.exitCode に入ってるのでOK
} finally {
run('docker', ['compose', '--profile', 'test', 'down', '--volumes', '--remove-orphans'], { allowFail: true })
process.exit(exitCode || process.exitCode || 0)
}
package.json
{
"scripts": {
"test:it": "node scripts/test-it.mjs"
}
}
これで👇が完成!🎉
npm run test:it
10) ミニ課題🎯(ここまでできたら強い)
課題A:制約テストを増やす🧩
nameを空で入れたら落ちる(NOT NULL)- 期待:テストがfailするのを確認 → 正しい入力だけ通す設計に寄せる🙂
課題B:テストデータの“片付け”を変える🧹
TRUNCATEを「各テスト(beforeEach)」→「各ファイル(beforeAll)」にして、速さの差を体感🏎️ (ただし独立性は下がるので、どっちが好みか考える)
11) よくある詰まり💣(だいたいここ)
❌ ECONNREFUSED / getaddrinfo ENOTFOUND db
- テストがコンテナ内ならホストは
db(サービス名) - ローカルから叩くなら
localhost:5432(ポート公開が必要)
❌ 初期SQLが反映されない
/docker-entrypoint-initdb.dは 空DBの時だけなので、データが残ってると動かない😇(Docker Hub)- 対策:
-Vを付ける /down --volumesで消す
❌ たまに落ちる(フレーク)
- 統合テストの並列が原因のこと多い
- 対策:
maxWorkers: 1(まず安定させる!)
12) AIで時短🤖✨(GitHub Copilot / OpenAI Codex 使いどころ)
使えるプロンプト例💡
- 「compose.yamlに test profile を追加して、dbのhealthcheckとdepends_on(service_healthy)付きで、testサービスが
npm run test:integrationを実行する形にして」 - 「Vitestの統合テスト用設定ファイルを作って。DB共有だから maxWorkers=1、timeout長め。includeは test/integration/** にしたい」
- 「UNIQUE制約違反をちゃんとテストで検知する例を書いて。落ちるのを
rejects.toThrow()で確認したい」
AI出力の“確認ポイント”✅
DATABASE_URLのホストが db になってる?healthcheckとdepends_on: condition: service_healthyがある?(Docker Documentation)- ボリュームが残って初期SQLが走らない罠にハマってない?(Docker Hub)
ここまでで「DBあり統合テストを、Composeで起動して、ワンコマンドで回す」基礎が完成です🧱🧪✨ 次の章(カバレッジ)に行く前に、**“統合テストは少数精鋭でいい”**感覚だけ掴めると超強いですよ😊