🙆‍♀️

🚀【NestJS × TypeORM × E2E】Docker 上でマスターデータ Seed & Mock 外部IF、GitHub Act

2024/12/16に公開

こんにちは!今回は NestJS を使った E2E テストのセットアップ手順をまとめます。
TypeORM を使って DB スキーマ管理 & Seed(マスターデータ投入)をしつつ、DB は Docker Volume なし で起動。さらに 必要に応じて外部APIをモック化 して、GitHub Actions で CI/CD を回す構成です。
リファクタリングや共通サービスの変更時デグレ防止 という背景を踏まえ、E2E テストのメリット についても触れていきます。
最後に サンプルテストコード を載せるので、ぜひ参考にしてください。


🎯 ゴール & 背景

E2E (End to End) テスト は、アプリケーションの入り口から外部連携を含めたシステム全体の挙動を検証するテスト手法です。ユニットテストや統合テストとは異なり、実際のリクエストをフレームワークに通してアプリを実行するため、リファクタリングや共通モジュールの変更があっても「全体として正常動作しているか」をチェックできます。

  • デグレ防止: リファクタリングや共通サービスの調整、DB スキーマ変更などでも、一通りのテストパスが通れば安心
  • 網羅性: 一つ一つのサービスやモジュール単体でのテストを全て書かなくても、E2E が通れば「最終的なユーザ視点での機能」が守られる
  • テスト作成工数の削減: ユニットテストを完璧に書くのは大変だけど、まずは E2E テストだけでも網羅しておけばある程度の品質が担保できる
  • 外部API連携 があっても、モック化することで速度・安定性・コストを確保できる

※ もちろん、機能規模やチームのテストポリシー次第で「単体テスト + E2E の組み合わせ」が望ましいケースも多いですが、優先順位をつける上では「まずは E2E だけでも確保」→「必要に応じてユニットテスト追加」というステップもアリです。


🛠️ 1. Docker & docker-compose 設定 (Volume なし)

E2E テスト用の PostgreSQL コンテナを一時的に使い捨てたいので、docker-composetmpfs を使う構成を例示します。

version: '3.8'
services:
  db:
    image: postgres:15-alpine
    container_name: nestjs_test_db
    environment:
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpassword
      POSTGRES_DB: testdb
    ports:
      - "5432:5432"
    tmpfs:
      - /var/lib/postgresql/data
  • tmpfs マウントを使うと、コンテナ停止と同時に DB データが消えるため、テストのたびにクリーンな状態を用意できます。
  • Volume を一切定義しないパターンでも一時的なコンテナストレージが使われるので似た運用は可能ですが、tmpfs は高速で分かりやすいです。

💾 2. NestJS + TypeORM 設定

2-1. .env.test を用意

# .env.test
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=testuser
DB_PASSWORD=testpassword
DB_DATABASE=testdb

E2E テストで使う専用環境変数。リファクタリングや DB スキーマ変更があっても、ここを変えるだけで済むので運用しやすいです。

2-2. TypeORM Config

// src/config/typeorm.config.ts
import { ConfigService } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export const typeOrmConfig = async (
  configService: ConfigService,
): Promise<TypeOrmModuleOptions> => {
  return {
    type: 'postgres',
    host: configService.get<string>('DB_HOST'),
    port: configService.get<number>('DB_PORT'),
    username: configService.get<string>('DB_USERNAME'),
    password: configService.get<string>('DB_PASSWORD'),
    database: configService.get<string>('DB_DATABASE'),
    entities: [__dirname + '/../**/*.entity.{js,ts}'],
    synchronize: false, // migrationを使う想定
  };
};

2-3. NestJS モジュール

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { typeOrmConfig } from './config/typeorm.config';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
    }),
    TypeOrmModule.forRootAsync({
      useFactory: typeOrmConfig,
      inject: [ConfigModule],
    }),
    // 他のモジュール...
  ],
})
export class AppModule {}

🌱 3. Migration & Seed でマスターデータ投入

3-1. Migration

TypeORM のマイグレーションで DB スキーマ を整えます。
リファクタリングでスキーマが変わっても、マイグレーションを修正すればテストが自動で追従し、デグレ検知にも役立ちます。

// ormconfig.ts
import { DataSource, DataSourceOptions } from 'typeorm';

const config: DataSourceOptions = {
  type: 'postgres',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT) || 5432,
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE,
  entities: ['src/**/*.entity.ts'],
  migrations: ['src/migrations/*.ts'],
};
export default new DataSource(config);
// package.json
{
  "scripts": {
    "migration:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:run -d ormconfig.ts"
  }
}

3-2. Seed (TypeORM-Seeding)

マスターデータを投入しておけば、E2E テストでアプリが依存する定数や設定を簡単に呼び出せます。
typeorm-seeding を導入:

npm install -D typeorm-seeding ts-node
// src/seeds/master.seed.ts
import { Factory, Seeder } from 'typeorm-seeding';
import { Connection } from 'typeorm';
import { Country } from '../entities/country.entity';

export default class CreateMasterSeed implements Seeder {
  public async run(_: Factory, connection: Connection): Promise<void> {
    await connection
      .createQueryBuilder()
      .insert()
      .into(Country)
      .values([
        { code: 'JP', name: 'Japan' },
        { code: 'US', name: 'United States' },
      ])
      .execute();
  }
}
{
  "scripts": {
    "seed:master": "ts-node -r tsconfig-paths/register ./node_modules/typeorm-seeding/dist/cli.js seed"
  }
}

リファクタリングで新たなマスタデータが増えたり変更になっても、Seed の定義を更新すればOK。
テスト前に migration:run & seed:master すれば常に最新のマスタ状態からE2Eを実行できます。


🧩 4. 外部IF の Mock 化

外部の共通サービスや外部APIと連携する場合、モックを使うことでテストの安定性と速度が向上します。もちろん実際に外部と疎通する本物の E2E にも利点がありますが、共通IF が頻繁に変更されるなら、モックを活用する方が回収が早い場合も多いです。

4-1. overrideProvider で NestJS のサービスをモック差し替え

// test/__mocks__/external.service.ts
export class ExternalServiceMock {
  async fetchData() {
    return { status: 'OK', data: 'Mocked' };
  }
}

// test/app.e2e-spec.ts
describe('App E2E with external mock', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [AppModule],
    })
      .overrideProvider(ExternalService)
      .useClass(ExternalServiceMock)
      .compile();

    app = moduleRef.createNestApplication();
    await app.init();
  });

  it('should pass with mock data', async () => {
    // テストロジック...
  });
});

モック化により、外部仕様が変更されてもアプリ内で受け取るデータ形式をテストで定義すればデグレを早期発見できます。


🧪 5. E2E テストコード例

実際の E2E テストコードはこんな感じ。下記では、Seed で投入した国マスタが正しく取得できるかをチェックしています。テスト前に npm run migration:run && npm run seed:master を済ませておけばOK。

// test/app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { AppModule } from '../src/app.module';
import * as request from 'supertest';

describe('Countries E2E', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it('/countries (GET) should return seed data', async () => {
    const response = await request(app.getHttpServer()).get('/countries');
    expect(response.status).toBe(200);
    expect(response.body).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ code: 'JP', name: 'Japan' }),
        expect.objectContaining({ code: 'US', name: 'United States' }),
      ]),
    );
  });
});

リファクタリングでこのエンドポイントやマスタ構造が変わったら、テストを修正すれば影響範囲が把握しやすく、デグレを防ぎやすいです。


🤖 6. GitHub Actions (CI/CD)

GitHub Actions で Pull Request 時にこれら E2E テストを回せば、共通モジュールの変更による影響DB マイグレーションの破壊的変更を自動検知できます。

6-1. docker-compose を使うワークフロー例

name: E2E Test

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  e2e-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 18

      - name: Install Dependencies
        run: npm install

      - name: Start DB via docker-compose
        run: docker-compose up -d

      - name: Wait for DB
        run: npx wait-on tcp:localhost:5432

      - name: Migration & Seed
        run: |
          npm run migration:run
          npm run seed:master

      - name: Run E2E Tests
        env:
          NODE_ENV: test
        run: npm run test:e2e

      - name: Stop DB
        run: docker-compose down

DBコンテナの起動 → マイグレーション → シード → テスト という流れを自動化。
リファクタしても PR 上でテストが落ちれば「どこに破壊的影響があるか」即座にわかり、デグレ防止につながります。


🍀 まとめ & 得られるメリット

  1. E2E テストによるデグレ防止
    • リファクタや共通サービスの変更時にも、E2E が通れば「アプリ全体が動いている」と確認できる
  2. マスターデータ Seed で DB を毎回整備
    • Docker tmpfs を使い捨て → テスト完了でデータ破棄
    • マイグレーション & Seed で常に最新状態を再現し、変更影響をすぐテスト
  3. 外部IF は Mock 化 すれば安定高速
    • 外部の仕様変更があれば Mock を更新しやすい
    • テスト作成工数をおさえつつ、必要最低限の結合保証ができる
  4. GitHub Actions で CI/CD
    • プルリクごとに E2E テストが自動で走る → デグレ即発見
    • テストが通るのを確認してから安心してリリース

E2E だけで全てをカバーできるわけではありませんが、「どこからどこまでをテストすればデグレに気づけるか」 という視点からすると、まずはE2Eを優先的に整備することは 工数対効果 が非常に高いです。モジュール単体テストを完璧に網羅しなくても、E2E が通ればユーザ機能が動いていることは保障されるという考え方もあります。

もちろんシステムの規模やチーム状況によっては単体テストが不可欠な場面もありますが、「E2E だけでも十分にデグレ検知・品質担保ができる」 と考える人も多いです。ぜひプロジェクトの方針に合わせて、本番運用にフィットしたテスト体系 を構築してみてくださいね。

Discussion