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?

バッチ処理の信頼性を高めるテスト戦略: Vitest + SQL直叩き二段構え

Posted at

1. はじめに {#introduction}

設計とパフォーマンスを整えたバッチでも、「ちゃんと同期されている?」を証明できなければ安心して運用できません。特に生SQLを直接書いている場合、TypeScript側のユニットテストだけではカバーできないギャップが残りがちです。

この記事では、NestJS + Prismaバッチを例に、**Vitest(ビジネスロジック)psql(SQLの実効検証)**を組み合わせたテスト戦略を紹介します。フレームワークに依存しない考え方なので、他言語・他ORMでも応用できます。なお、ジョブキューは BullMQ + Redis 上で動かす想定ですが、テストの焦点は「キュー投入 → Worker実行 → DB反映」の一連をどう担保するかに置いています。

1.1 バックエンドテスト気づいたこと {#takeaways}

  • バッチ処理のユースケースに合わせたVitestの書き方
  • psqlを使ったSQLレベルの直接テスト(シェルスクリプト化)
  • ファクトリ・マスターデータ・依存関係を整えるコツ
  • 時刻固定や冪等性確認など、バッチ特有のテストケース
  • CI/CDへの統合例とトラブルシューティング

2. なぜVitestだけでは十分でないのか {#why-two-layers}

2.1 見逃しがちなポイント {#blind-spots}

項目 ユニットテストだけでは見えない例
SQL構文 JOIN順のミス、CTEの書き間違い
制約 外部キー・ユニーク制約の違反
PostgreSQL固有 IS DISTINCT FROMの挙動、make_dateなど
実行計画 意図せぬSeq Scan、INDEX未活用

Vitestで検証できるのは「サービス層の振る舞い」まで。生SQLの品質は実データで確認する必要があります。

3. Vitestで押さえるべき基本 {#vitest}

3.1 テスト構造 {#structure}

import { beforeEach, describe, expect, it } from 'vitest';
import { resetSequence } from '@/test/factories/helpers';
import { ProjectFactory } from '@/test/factories/project';

describe('ProjectSyncService', () => {
  let service: ProjectSyncService;

  beforeEach(async () => {
    await resetSequence();
    service = createService();
  });

  it('差分があるレコードだけUPDATEする', async () => {
    await ProjectFactory.create();
    await service.syncAll();

    const rows = await prisma.targetMonthly.findMany();
    expect(rows).toHaveLength(1);
  });
});
  • BullMQ を使っている場合も、Processor(Worker)クラスを直接呼び出す形にしておくと、キューモジュールを立ち上げずに振る舞いを検証できる
  • Factoryで依存関係を組み立て、外部キーを気にせずデータを作る
  • resetSequence()などを使ってID衝突を防ぐ

3.2 キュー投入のテスト観点 {#queue-tests}

  • 単体テスト: BullMQのQueueWorkerをモックし、Processorのハンドラを直接コールする
  • 結合テスト的に確認したい場合: 低頻度で実行される専用describeを用意し、テストRedisを起動してキュー→Worker→DBまで確認する(必要に応じて@bull-board/apiなどを利用)

3.3 時刻固定の重要性 {#fake-time}

import { vi } from 'vitest';

beforeEach(() => {
  vi.useFakeTimers();
  vi.setSystemTime(new Date('2025-10-01T00:00:00Z'));
});

afterEach(() => {
  vi.useRealTimers();
});
  • ウォーターマーク比較、月末・年度跨ぎなど境界テストには必須
  • CIでも安定して動作させるための基本テク

3.4 冪等性テスト {#idempotent}

it('同じ同期を2回叩いても更新日時が変わらない', async () => {
  await service.syncAll();
  const first = await prisma.targetMonthly.findMany({ select: { updatedAt: true } });

  await service.syncAll();
  const second = await prisma.targetMonthly.findMany({ select: { updatedAt: true } });

  expect(second).toEqual(first);
});

4. SQL直接テストの構成 {#sql-tests}

4.1 シェルスクリプトで包む {#shell}

Vitestでは拾いきれない部分は、psqlを使って「マスターデータ投入→SQL実行→結果検証」を自動化します。

#!/usr/bin/env bash
set -euo pipefail

DB_URL=$(echo "$DATABASE_URL" | sed -E 's/\?schema=[^&]*//')

cleanup() {
  psql "$DB_URL" -c "TRUNCATE TABLE target_monthly CASCADE;" >/dev/null
  # 依存テーブルも順番にTRUNCATE
}

run_query() {
  psql "$DB_URL" <<'SQL'
  -- バッチと同じSQLを実行
  UPDATE target_monthly ...;
SQL
}

assert_eq() {
  local expected="$1" actual="$2" name="$3"
  if [[ "$expected" == "$actual" ]]; then
    echo "✅ $name"
  else
    echo "❌ $name\n expected: $expected\n actual: $actual"
    exit 1
  fi
}

4.2 マスターデータの注入 {#seed}

  • 外部キーを意識して依存順にINSERT
  • ON CONFLICT DO NOTHINGON DELETE SET NULLなど、実装の挙動に合わせる
  • テスト終わりにTRUNCATE ... CASCADEでクリーンアップ

4.3 ケース設計 {#cases}

# ケース 観点
1 既存値を差分UPDATE IS DISTINCT FROMで差分検知できるか
2 欠損行をINSERT createManyでまとめて挿入されるか
3 同じ値の再実行 冪等性が担保されているか
4 NULL → 値 NULL比較を正しく扱えるか
5 範囲外データ start/end期間から外れるデータをスキップできるか

4.4 DevContainerからpsqlを叩くときのコツ {#devcontainer-psql}

開発コンテナ(例: VS Code Dev Containers)でPostgreSQLへアクセスする場合、PrismaのDATABASE_URLには?schema=...が付与されていることが多いので、psql実行時に取り除くのがポイントです。

export DATABASE_URL='postgresql://user:pass@db:5432/database?schema=public'

# schemaクエリパラメータを除去してSQLファイルを実行
psql "$(printf '%s' "$DATABASE_URL" | sed -E 's/\?schema=[^&]*//')" \
  -f /workspace/apps/api/src/cli/monthly-snapshot.test.sql
  • ファイルパスエラーが出る場合: -f ./src/...の代わりに、/workspace/... の絶対パスを渡すと解決することが多い
  • 環境に合わせてDATABASE_URLのユーザー名/パスワード/DB名を調整してください
  • 1回限りのスクリプト実行なら cat sql | psql ... でもOKですが、-fでファイル指定しておくと再利用しやすい

5. VitestとSQLテストの役割分担 {#division}

観点 Vitest SQLテスト
ビジネスロジック
Prisma呼び出し・例外 -
SQL構文/CTE/関数 -
制約・依存関係
実行時間計測 ✅(EXPLAIN ANALYZEで確認可能)

両方を組み合わせることで、アプリ層/SQL層それぞれのバグを早期に検知できます。リリース前にどちらか一方だけで済ませず、定期的に双方のテストを走らせる運用を目指しましょう。

6. CI/CDへの組み込み {#ci}

name: batch-tests

on:
  pull_request:
    paths:
      - "apps/api/src/batch/**"
      - "apps/api/test-scripts/**"

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - run: pnpm install
      - run: pnpm vitest run
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
      - run: ./apps/api/test-scripts/batch-sql-tests.sh
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
  • Postgresサービスを立ち上げてSQLテスト用のDBを確保
  • Vitestでビジネスロジックを検証したあとにSQLテストを走らせる
  • どちらか一方でも失敗したらPRをブロックする仕組みにしておく

7. トラブルシューティング {#troubleshooting}

7.1 代表的なエラー {#common-errors}

  • 外部キー違反: マスターデータの挿入順序を見直す
  • カラム存在エラー: PrismaスキーマとSQLスキーマのズレを確認する
  • psql ... schemaエラー: ?schema= パラメータを削除してから接続する(4.4参照)

7.2 デバッグTips {#debug}

# 詳細エラーを有効化
psql "$DB_URL" <<'SQL'
\set ON_ERROR_STOP on
\set VERBOSITY verbose
-- 検証したいSQLをここに貼る
SQL

# 変更を残したくない場合はトランザクションで囲んでROLLBACK
psql "$DB_URL" <<'SQL'
BEGIN;
-- テストSQL
ROLLBACK;
SQL

8. チェックリスト {#checklist}

  • Factoryで依存データを自動生成できる
  • Vitestで冪等性・境界条件・例外を網羅
  • SQLテストで実際のクエリを検証している
  • DevContainerなどから psql を安全に叩ける(スクリプト化済み)
  • CIでVitestとSQLテストが両方実行される
  • 実行ログや計測結果を追ってパフォーマンス劣化を検知できる

9. まとめ {#conclusion}

  1. アプリ層(Vitest)とSQL層(psqlテスト)の二段構えで品質を担保する
  2. BullMQのProcessorは直接呼び出せるように設計し、キュー環境がなくてもテストを回せるようにする
  3. CIに組み込み、常に両方のテストが走る状態を保つことで安心してリリースできる

これで、設計(パート1)・最適化(パート2)・テスト(本記事)の3本柱が揃いました。自分のチームのバッチ処理に合わせてアレンジしつつ、ぜひ活用してみてください!


関連記事

  1. プロジェクト単位で失敗に強いバッチ設計を組む方法
  2. バッチ処理を10倍速にする5つのテクニック
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?