0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

使い捨てテストDB環境のtestcontainersをPostgresql + Honoで試してみたメモ

Last updated at Posted at 2025-04-07

概要

コンテナを立ち上げてDBテストを行ってみた。

この時のソース

テーブルの初期化がテストに含まれていると可読性が悪い。次の記事で解決した。

ソース

apps/backend/package.json
{
  "scripts": {
    "test": "vitest --config vitest.config.worker.mts",
+   "integration-test": "DEBUG=testcontainers* vitest run --config vitest.config.integration.mts"
  },
  "devDependencies": {
    "@cloudflare/vitest-pool-workers": "^0.8.11",
    "@cloudflare/workers-types": "^4.20250405.0",
+   "@testcontainers/postgresql": "^10.24.0",
    "dotenv": "^16.4.7",
+   "pg": "^8.14.1",
+   "testcontainers": "^10.24.0",
    "vitest": "3.1.1",
    "wrangler": "^4.7.2"
  },
  "dependencies": {
    "@hono/valibot-validator": "^0.5.2",
    "hono": "^4.7.5",
    "@odyssage/database": "workspace:*",
    "@odyssage/lib": "workspace:*",
    "@odyssage/schema": "workspace:*"
  }
}
apps/backend/test/integration.spec.ts
import {
  PostgreSqlContainer,
  StartedPostgreSqlContainer,
} from '@testcontainers/postgresql';
import { Hono } from 'hono';
import { Client } from 'pg';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { sessionRoute } from '../src/route/session';
import { generateUUID } from '../src/utils/generateUUID';

/**
 * セッション関連のエンドポイントに対する統合テスト
 * Testcontainersを使用して実際のPostgreSQLコンテナを起動し、
 * データベース操作を含む統合テストを実行します
 */
describe('セッション統合テスト', () => {
  let postgresContainer: StartedPostgreSqlContainer;

  let connectionString: string;
  let pgClient: Client;
  let app: Hono;

  // テストユーザーとシナリオの情報
  const testUserId = 'test-user-id';
  const testUserName = 'テストユーザー';
  const testScenarioId = generateUUID();
  const testScenarioTitle = 'テストシナリオ';

  // テスト開始前にPostgreSQLコンテナを起動
  beforeAll(async () => {
    postgresContainer = await new PostgreSqlContainer('postgres:17.4-alpine')
      .withDatabase('test_db')
      .withUsername('test_user')
      .withPassword('test_password')
      .start();

    connectionString = postgresContainer.getConnectionUri();

    // PGクライアントを作成してテスト用のテーブルを初期化
    pgClient = new Client({
      connectionString,
    });

    await pgClient.connect();

    // スキーマとテーブルを作成
    await pgClient.query(`CREATE SCHEMA odyssage;`);

    await pgClient.query(`
      CREATE TABLE odyssage.users (
        id VARCHAR(64) PRIMARY KEY,
        name TEXT NOT NULL DEFAULT ''
      );
    `);

    await pgClient.query(`
      CREATE TABLE odyssage.scenarios (
        id UUID PRIMARY KEY,
        title TEXT NOT NULL,
        user_id VARCHAR(64) NOT NULL REFERENCES odyssage.users(id) ON DELETE CASCADE,
        created_at TIMESTAMP NOT NULL DEFAULT NOW(),
        updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
        overview TEXT NOT NULL DEFAULT '',
        visibility VARCHAR(10) NOT NULL DEFAULT 'private'
      );
    `);

    await pgClient.query(`
      CREATE TABLE odyssage.sessions (
        id UUID PRIMARY KEY,
        gm_id VARCHAR(64) NOT NULL REFERENCES odyssage.users(id) ON DELETE CASCADE,
        scenario_id UUID NOT NULL REFERENCES odyssage.scenarios(id) ON DELETE CASCADE,
        title TEXT NOT NULL,
        status VARCHAR(10) NOT NULL DEFAULT '準備中',
        created_at TIMESTAMP NOT NULL DEFAULT NOW(),
        updated_at TIMESTAMP NOT NULL DEFAULT NOW()
      );
    `);

    // テストデータを作成
    await pgClient.query(
      `
      INSERT INTO odyssage.users (id, name) VALUES ($1, $2)
    `,
      [testUserId, testUserName],
    );

    await pgClient.query(
      `
      INSERT INTO odyssage.scenarios (id, title, user_id) VALUES ($1, $2, $3)
    `,
      [testScenarioId, testScenarioTitle, testUserId],
    );

    // アプリを初期化
    app = new Hono();
    app.route('/api/sessions', sessionRoute);
  }, 60000); // コンテナ起動に時間がかかるためタイムアウトを延長

  // テスト終了後にコンテナを停止
  afterAll(async () => {
    await pgClient.end();
    await postgresContainer.stop();
  });

  it('セッションを作成して正しく取得できること', async () => {

    const sessionData = {
      gm_id: testUserId,
      scenario_id: testScenarioId,
      title: 'テストセッション',
    };
    const env = {
      CLOUDFLARE_ENV: 'test',
      NEON_CONNECTION_STRING: connectionString,
    };
    const postResponse = await app.request(
      '/api/sessions',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(sessionData),
      },
      env, // 第3引数に入れると環境変数を設定できる
    );
    expect(postResponse.status).toBe(201);

    const createdSession = (await postResponse.json()) as any;

    const getResponse = await app.request(
      `/api/sessions/${createdSession.id}`,
      undefined,
      env,
    );

    expect(getResponse.status).toBe(200);
  });
});
apps/backend/vitest.config.integration.mts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['test/integration.spec.ts'],
    // 統合テストではNode.jsのAPIを使用するので、ポリフィルを有効化
    deps: {
      interopDefault: true,
    },
    environment: 'node',
    environmentOptions: {
      // Bunでfs/promisesなどのNode.jsモジュールを利用可能にする
      testBunIntegration: true,
    },
    // コンソールログを抑制する設定
    silent: false,
    // テストタイムアウトを延長(Testcontainersはコンテナの起動に時間がかかる)
    testTimeout: 60000,
  },
});

neonのプロキシをDockerでうまく上げられなかったので、testcontainersはdrizzle-orm/postgres-jsを使ってアクセスすることにした。*

packages/datbase/src/db.ts
import { neon, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
+ import { drizzle as nodePostgresDrizzle } from 'drizzle-orm/postgres-js';
import type { NeonQueryFunction } from '@neondatabase/serverless';
import type { NeonHttpDatabase } from 'drizzle-orm/neon-http';

type NeonDBClient = NeonHttpDatabase<Record<string, never>> & {
  $client: NeonQueryFunction<false, false>;
};

let db: NeonDBClient | null = null;

export const getDb = (
  connectionString = process.env.NEON_CONNECTION_STRING!,
) => {
  if (db != null) {
    return db;
  }
+  if (connectionString.startsWith('postgres://test_user')) {
+    console.log('testcontainersで起動したPostgreSQLコンテナを使用します');
+    db = nodePostgresDrizzle(connectionString) as unknown as NeonDBClient;
+    return db;
+  }
  if (
    connectionString ===
    'postgres://postgres:postgres@db.localtest.me:5432/main'
  ) {
    console.log('ローカルのPostgreSQLコンテナを使用します');
    neonConfig.fetchEndpoint = `http://db.localtest.me:4444/sql`;
    neonConfig.useSecureWebSocket = false;
  }
  const sql = neon(connectionString);
  db = drizzle({ client: sql });
  return db;
};

参考

Testcontainersを用いたNext.jsとDBの結合テスト
Testcontainersで実現する、使い捨て結合テスト環境構築とテスト実施

Neon(Postgres)のDockerをローカルで稼働させ、Drizzleを使ったマイグレーションとCloudflare Workersのローカル実行からの接続を行ったメモ

testcontainers
postgresql - docker

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?