コンテンツにスキップ

Vendure開発ハンドブック

このドキュメントは、Vendureでの開発に必要なコアコンセプトと高度な実装パターンを体系的にまとめた知識ベースです。プロジェクト固有のセットアップについては Vendure開発ガイド を参照してください。

目次

アーキテクチャ概要

ヘッドレスアーキテクチャとGraphQL

Vendureは完全なヘッドレスアーキテクチャを採用しており、すべての機能はGraphQL APIを通じて提供されます。

2つのAPIエンドポイント

  • Shop API (/shop-api): ストアフロント(顧客向け)用のAPI
  • 認証: Customer認証(Cookie/Bearer Token)
  • 用途: 商品閲覧、カート操作、注文作成
  • 権限: 顧客がアクセス可能なデータのみ

  • Admin API (/admin-api): 管理画面(管理者向け)用のAPI

  • 認証: Administrator認証(Cookie/Bearer Token)
  • 用途: 商品管理、注文管理、顧客管理、設定変更
  • 権限: Role-Based Access Control (RBAC)

GraphQLスキーマの自動生成

Vendureはプラグインで定義されたGraphQLスキーマを自動的に統合し、単一のスキーマとして提供します。

import { gql } from 'graphql-tag';

const myGraphqlSchema = gql`
  extend type Query {
    myQuery: String!
  }

  extend type Mutation {
    myMutation(input: String!): Boolean!
  }
`;

@VendurePlugin({
  shopApiExtensions: {
    schema: myGraphqlSchema,
    resolvers: [MyResolver],
  },
})
export class MyPlugin {}

オニオンアーキテクチャとサービスレイヤーパターン

Vendureはオニオンアーキテクチャ(Onion Architecture)を採用し、依存関係の方向を明確にしています。

レイヤー構造

┌─────────────────────────────────────┐
│   GraphQL Resolvers (API Layer)     │
├─────────────────────────────────────┤
│   Services (Business Logic)         │
├─────────────────────────────────────┤
│   Entities (Domain Models)           │
├─────────────────────────────────────┤
│   TypeORM (Data Access)             │
└─────────────────────────────────────┘

依存関係の原則

  • 外側から内側への依存: Resolver → Service → Entity
  • 内側から外側への依存は禁止: EntityがServiceやResolverに依存しない
  • DIコンテナ: NestJSの依存性注入システムを使用

プラグインシステムの全体像

Vendureのすべての機能はプラグインとして実装されます。プラグインは以下の要素で構成されます:

プラグインの構成要素

@VendurePlugin({
  // 1. エンティティ定義
  entities: [MyCustomEntity],

  // 2. サービス・プロバイダー
  imports: [PluginCommonModule],
  providers: [MyService],

  // 3. GraphQL API拡張
  shopApiExtensions: {
    schema: shopSchema,
    resolvers: [ShopResolver],
  },
  adminApiExtensions: {
    schema: adminSchema,
    resolvers: [AdminResolver],
  },

  // 4. 設定の拡張
  configuration: config => {
    config.customFields.Customer.push({
      /* ... */
    });
    return config;
  },
})
export class MyPlugin {}

コアコンセプト詳細

Request Context (ctx)

RequestContextは、すべてのVendure操作の中心となるオブジェクトです。リクエストごとに作成され、言語、チャネル、ユーザー情報などのコンテキストを保持します。

RequestContextの主要プロパティ

interface RequestContext {
  // チャネル情報
  channel: Channel;
  channelId: ID;

  // 言語情報
  languageCode: LanguageCode;

  // ユーザー情報
  user?: User;
  activeUserId?: ID;

  // 認証情報
  isAuthorized: boolean;
  apiType: 'shop' | 'admin';

  // セッション情報
  session?: Session;
  sessionCache?: SessionCache;
}

RequestContextの使用方法

すべてのサービスメソッドは、最初の引数としてRequestContextを受け取ります:

@Injectable()
export class MyService {
  constructor(private connection: TransactionalConnection) {}

  async getData(ctx: RequestContext, id: string): Promise<MyEntity> {
    // ctx.channelId でチャネルを識別
    // ctx.languageCode で言語を識別
    // ctx.user でユーザー情報を取得

    return this.connection
      .getRepository(ctx, MyEntity)
      .findOne({ where: { id, channelId: ctx.channelId } });
  }
}

RequestContextのライフサイクル

  1. リクエスト受信: HTTPリクエストが到着
  2. Context作成: チャネル、言語、ユーザー情報からRequestContextを生成
  3. Resolver実行: GraphQL Resolverにctxが渡される
  4. Service呼び出し: Serviceメソッドにctxが渡される
  5. トランザクション管理: ctxに紐づくトランザクションが管理される

トランザクション管理

RequestContextは、リクエスト全体を通じて同じデータベーストランザクションを共有します:

async function processOrder(ctx: RequestContext, orderId: string) {
  // すべての操作が同じトランザクション内で実行される
  const order = await this.orderService.findOne(ctx, orderId);
  await this.paymentService.process(ctx, order);
  await this.fulfillmentService.create(ctx, order);
  // エラーが発生した場合、すべてロールバックされる
}

Event Bus

Vendureはイベント駆動アーキテクチャを採用しており、EventBusを通じて非同期処理や拡張ポイントを提供します。

イベントの種類

  • 同期イベント: リクエスト処理中に同期的に発火(例: OrderPlacedEvent
  • 非同期イベント: バックグラウンドで処理(例: OrderStateTransitionEvent

イベントの購読

import { EventBus, OrderPlacedEvent } from '@vendure/core';

@Injectable()
export class MyService {
  constructor(private eventBus: EventBus) {
    // イベントの購読
    this.eventBus.ofType(OrderPlacedEvent).subscribe(event => {
      console.log('Order placed:', event.entity.id);
      // カスタム処理を実行
    });
  }
}

カスタムイベントの作成

import { VendureEvent } from '@vendure/core';

export class CustomOrderEvent extends VendureEvent {
  constructor(
    public ctx: RequestContext,
    public order: Order,
    public customData: any,
  ) {
    super();
  }
}

// イベントの発火
this.eventBus.publish(new CustomOrderEvent(ctx, order, data));

イベントのベストプラクティス

  • 重い処理は非同期: メール送信、外部API呼び出しなどは非同期イベントを使用
  • エラーハンドリング: イベントハンドラ内のエラーは適切にキャッチ
  • 順序保証: イベントの処理順序は保証されないため、依存関係に注意

Worker System

Vendureは非同期ジョブキューシステムを提供し、重い処理をバックグラウンドで実行できます。

Job Queue Plugin

// vendure-config.ts
import { DefaultJobQueuePlugin } from '@vendure/core';

export const config: VendureConfig = {
  plugins: [
    DefaultJobQueuePlugin.init({
      pollInterval: 2000, // ポーリング間隔(ミリ秒)
    }),
  ],
};

ジョブの作成と実行

import { JobQueue, JobQueueService } from '@vendure/core';

@Injectable()
export class MyService {
  constructor(private jobQueueService: JobQueueService) {}

  async processLargeData(ctx: RequestContext, data: any) {
    // ジョブをキューに追加
    const job = await this.jobQueueService.add({
      name: 'process-large-data',
      data: { dataId: data.id },
      retries: 3,
    });

    return job;
  }
}

カスタムジョブプロセッサー

import { Job, JobQueue, JobQueueService } from '@vendure/core';

@Injectable()
export class MyJobProcessor {
  async process(job: Job): Promise<void> {
    const { dataId } = job.data;
    // 重い処理を実行
    await this.processData(dataId);
  }
}

// プラグインで登録
@VendurePlugin({
  providers: [MyJobProcessor],
  jobQueueOptions: {
    process: async job => {
      const processor = app.get(MyJobProcessor);
      return processor.process(job);
    },
  },
})
export class MyPlugin {}

Strategies

VendureはConfigurableOperationDefを使用して、特定のロジックを差し替え可能にする「Strategy」パターンを提供します。

Strategyの例: Shipping Calculator

import { ShippingCalculator, ShippingCalculatorArgs } from '@vendure/core';

export class CustomShippingCalculator implements ShippingCalculator {
  code = 'custom-shipping';
  description = [{ languageCode: LanguageCode.ja, value: 'カスタム配送計算' }];

  calculate(
    ctx: RequestContext,
    args: ShippingCalculatorArgs,
  ): number | Promise<number> {
    // カスタム配送料計算ロジック
    const basePrice = 500;
    const weight = args.orderLines.reduce(
      (sum, line) => sum + line.productVariant.weight,
      0,
    );
    return basePrice + weight * 100;
  }
}

// 設定で登録
export const config: VendureConfig = {
  shippingOptions: {
    shippingCalculators: [CustomShippingCalculator],
  },
};

Strategyの実装パターン

  1. インターフェース実装: ShippingCalculatorPaymentMethodHandlerなどのインターフェースを実装
  2. 設定で登録: vendure-config.tsでStrategyを登録
  3. 動的選択: 管理者がDashboardでStrategyを選択可能

データアクセスと拡張

TransactionalConnection

TransactionalConnectionは、Vendureが提供するトランザクションセーフなデータベースアクセス層です。

基本的な使用方法

import { TransactionalConnection } from '@vendure/core';

@Injectable()
export class MyService {
  constructor(private connection: TransactionalConnection) {}

  async getEntity(ctx: RequestContext, id: string): Promise<MyEntity> {
    // Repositoryを取得
    const repo = this.connection.getRepository(ctx, MyEntity);

    // クエリ実行
    return repo.findOne({ where: { id } });
  }

  async createEntity(
    ctx: RequestContext,
    data: Partial<MyEntity>,
  ): Promise<MyEntity> {
    const repo = this.connection.getRepository(ctx, MyEntity);
    const entity = repo.create(data);
    return repo.save(entity);
  }
}

トランザクションの明示的管理

async function complexOperation(ctx: RequestContext) {
  // 新しいトランザクションを開始
  return this.connection.withTransaction(ctx, async transactionCtx => {
    const entity1 = await this.createEntity1(transactionCtx, data1);
    const entity2 = await this.createEntity2(transactionCtx, data2);
    // エラーが発生した場合、すべてロールバック
    return { entity1, entity2 };
  });
}

クエリビルダーの使用

async function complexQuery(ctx: RequestContext) {
  const repo = this.connection.getRepository(ctx, Order);

  return repo
    .createQueryBuilder('order')
    .leftJoinAndSelect('order.lines', 'line')
    .leftJoinAndSelect('line.productVariant', 'variant')
    .where('order.state = :state', { state: 'PaymentSettled' })
    .andWhere('order.createdAt > :date', { date: new Date('2024-01-01') })
    .getMany();
}

Custom Fields

Custom Fieldsを使用すると、Vendureの標準エンティティに独自のフィールドを追加できます。

Custom Fieldsの定義

// vendure-config.ts
export const config: VendureConfig = {
  customFields: {
    Customer: [
      {
        name: 'customerStatus',
        type: 'string',
        defaultValue: 'general',
        label: [{ languageCode: LanguageCode.ja, value: '顧客ステータス' }],
        options: [
          {
            value: 'general',
            label: [{ languageCode: LanguageCode.ja, value: '一般' }],
          },
          {
            value: 'premium',
            label: [{ languageCode: LanguageCode.ja, value: 'プレミアム' }],
          },
        ],
      },
      {
        name: 'discountRate',
        type: 'float',
        defaultValue: 0,
        min: 0,
        max: 100,
      },
    ],
    Product: [
      {
        name: 'minimumOrderQuantity',
        type: 'int',
        defaultValue: 1,
      },
    ],
  },
};

プラグインでのCustom Fields追加

@VendurePlugin({
  configuration: config => {
    config.customFields.Customer = config.customFields.Customer ?? [];
    config.customFields.Customer.push({
      name: 'customField',
      type: 'string',
    });
    return config;
  },
})
export class MyPlugin {}

TypeScript型定義

Custom Fieldsを使用する場合、TypeScriptの型定義を拡張する必要があります:

// src/types/vendure-custom-fields.ts
declare module '@vendure/core' {
  interface CustomCustomerFields {
    customerStatus?: string;
    discountRate?: number;
  }

  interface CustomProductFields {
    minimumOrderQuantity?: number;
  }
}

Custom Fieldsの使用

// 読み取り
const customer = await this.customerService.findOne(ctx, customerId);
const status = customer.customFields.customerStatus;

// 更新
await this.customerService.update(ctx, customerId, {
  customFields: {
    customerStatus: 'premium',
    discountRate: 10,
  },
});

Entity Translation

Vendureは多言語対応エンティティをサポートしており、Translationエンティティを使用して複数言語のデータを管理します。

翻訳可能なエンティティ

  • Product
  • ProductVariant
  • Collection
  • Facet
  • FacetValue
  • ShippingMethod
  • PaymentMethod
  • Promotion

翻訳データの作成

await this.productService.create(ctx, {
  translations: [
    {
      languageCode: LanguageCode.ja,
      name: '商品名(日本語)',
      description: '説明(日本語)',
    },
    {
      languageCode: LanguageCode.en,
      name: 'Product Name (English)',
      description: 'Description (English)',
    },
  ],
});

翻訳データの取得

// RequestContextのlanguageCodeに基づいて自動的に翻訳が選択される
const product = await this.productService.findOne(ctx, productId);
// product.name は ctx.languageCode に応じた翻訳が返される

// 特定の言語の翻訳を取得
const japaneseTranslation = product.translations.find(
  t => t.languageCode === LanguageCode.ja,
);

プラグイン開発実践

ライフサイクルフック

プラグインは、Vendureの起動・停止時に実行されるライフサイクルフックを提供します。

利用可能なフック

@VendurePlugin({})
export class MyPlugin {
  // プラグインの初期化時(サーバー起動時)
  static async onBootstrap(app: INestApplication): Promise<void> {
    // 初期化処理
  }

  // Workerプロセスの作成時
  static async onWorkerCreation(app: INestApplication): Promise<void> {
    // Worker固有の初期化処理
  }

  // プラグインの終了時
  static async onClose(app: INestApplication): Promise<void> {
    // クリーンアップ処理
  }
}

実装例

@VendurePlugin({})
export class MyPlugin {
  static async onBootstrap(app: INestApplication): Promise<void> {
    const configService = app.get(ConfigService);
    const logger = app.get(Logger);

    logger.log('MyPlugin initialized', 'MyPlugin');

    // 外部サービスへの接続など
    await this.connectToExternalService(configService);
  }

  static async onClose(app: INestApplication): Promise<void> {
    // 接続の切断など
    await this.disconnectFromExternalService();
  }
}

依存性注入 (DI) のベストプラクティス

VendureはNestJSの依存性注入システムを使用します。

プロバイダーの登録

@VendurePlugin({
  imports: [PluginCommonModule], // Vendureの共通モジュール
  providers: [MyService, MyRepository],
})
export class MyPlugin {}

サービスの注入

@Injectable()
export class MyService {
  constructor(
    private connection: TransactionalConnection,
    private eventBus: EventBus,
    private logger: Logger,
  ) {}
}

スコープ管理

  • Singleton (デフォルト): アプリケーション全体で1つのインスタンス
  • Request: リクエストごとに新しいインスタンス
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
  // リクエストごとに新しいインスタンスが作成される
}

プラグインの構造

推奨ディレクトリ構造

packages/plugins/my-plugin/
├── src/
│   ├── index.ts              # プラグイン定義
│   ├── entities/             # カスタムエンティティ
│   │   └── my-entity.entity.ts
│   ├── services/             # ビジネスロジック
│   │   └── my.service.ts
│   ├── api/                  # GraphQL Resolver
│   │   ├── my.resolver.ts
│   │   └── my.schema.ts
│   └── types.ts              # 型定義
└── package.json

実装例

// src/index.ts
import { PluginCommonModule, VendurePlugin } from '@vendure/core';
import { MyEntity } from './entities/my-entity.entity';
import { MyService } from './services/my.service';
import { MyResolver } from './api/my.resolver';
import { myGraphqlSchema } from './api/my.schema';

@VendurePlugin({
  imports: [PluginCommonModule],
  entities: [MyEntity],
  providers: [MyService],
  shopApiExtensions: {
    schema: myGraphqlSchema,
    resolvers: [MyResolver],
  },
  compatibility: '^3.5.0',
})
export class MyPlugin {}

Dashboard開発 (React)

Vendure DashboardはReactベースの管理画面です。旧Admin UI(Angular)は使用せず、Dashboardのみを使用します。

基本構造

Dashboard Pluginの設定

// vendure-config.ts
import { DashboardPlugin } from '@vendure/dashboard/plugin';
import { join } from 'node:path';

export const config: VendureConfig = {
  plugins: [
    DashboardPlugin.init({
      route: 'dashboard',
      appDir: join(__dirname, '../dist/dashboard'),
    }),
  ],
};

ディレクトリ構造

apps/vendure-server/
├── src/
│   ├── plugins/
│   │   └── ritsubi-admin-extensions/
│   │       └── dashboard/          # カスタムDashboard拡張
│   │           ├── index.tsx       # Dashboard拡張のエントリーポイント
│   │           ├── components/     # カスタムコンポーネント
│   │           ├── utils/          # 共通ユーティリティ
│   │           │   └── download-helper.ts  # ファイルダウンロードヘルパー
│   │           └── i18n/           # 翻訳ファイル
│   │               ├── ja.po
│   │               └── en.po
│   ├── dashboard/                  # カスタムDashboard拡張(旧構成、移行予定)
│   │   ├── index.tsx
│   │   └── i18n/
│   │       ├── ja.po
│   │       └── en.po
│   └── vendure-config.ts
├── lingui.config.js            # Lingui設定
└── vite.config.mts            # Vite設定

拡張方法

ページブロックの追加

既存のページにカスタムブロックを追加できます:

// src/dashboard/index.tsx
import { defineDashboardExtension } from '@vendure/dashboard';
import { CustomerPasswordAdminBlock } from './components/customer-password-admin';

export default defineDashboardExtension({
  pageBlocks: [
    {
      id: 'customer-password-admin',
      title: '初期パスワード設定',
      location: {
        pageId: 'customer-detail',
        column: 'side',
        position: { blockId: 'groups', order: 'after' },
      },
      component: CustomerPasswordAdminBlock,
      shouldRender: context => Boolean(context.entity?.id),
      requiresPermission: ['UpdateCustomer'],
    },
  ],
});

新しいページの追加

export default defineDashboardExtension({
  routes: [
    {
      path: '/my-custom-page',
      component: () => <MyCustomPage />,
      loader: () => ({ breadcrumb: 'カスタムページ' }),
      navMenuItem: {
        sectionId: 'settings',
        title: 'カスタム設定',
      },
    },
  ],
});

ナビゲーションメニューの追加

export default defineDashboardExtension({
  navSections: [
    {
      id: 'integrations',
      title: '連携',
      icon: Cable, // lucide-reactのアイコン
    },
  ],
  routes: [
    {
      path: '/smile-sync',
      component: () => <SmileSyncPage />,
      navMenuItem: {
        sectionId: 'integrations',
        title: 'SMILE連携',
      },
    },
  ],
});

公式UIコンポーネント

Dashboard拡張では、@vendure/dashboardの公式UIコンポーネントを優先的に使用します。

利用可能なコンポーネント

import {
  Button,
  Input,
  Select,
  Card,
  Table,
  Form,
  Badge,
  Dialog,
  // ... その他多数
} from '@/vdb/components/ui/...';

レイアウトコンポーネント

import {
  Page,
  PageTitle,
  PageLayout,
  PageBlock,
} from '@/vdb/framework/...';

export function MyCustomPage() {
  return (
    <Page>
      <PageTitle>カスタムページ</PageTitle>
      <PageLayout>
        <PageBlock>
          {/* コンテンツ */}
        </PageBlock>
      </PageLayout>
    </Page>
  );
}

国際化 (i18n)

Dashboard拡張では、LinguiJSを使用して国際化を実装します。

翻訳キーの定義

import { Trans, useLingui } from '@lingui/react/macro';

export function MyComponent() {
  const { t } = useLingui();

  return (
    <div>
      <Trans>こんにちは</Trans>
      <p>{t`現在の日時: ${new Date().toLocaleString()}`}</p>
    </div>
  );
}

翻訳ファイルの管理

# 翻訳キーの抽出
npx lingui extract

# 翻訳のコンパイル
npx lingui compile

翻訳ファイルは src/dashboard/i18n/ に配置されます:

src/dashboard/i18n/
├── ja.po
└── en.po

翻訳ファイルの編集

# ja.po
msgid "Hello"
msgstr "こんにちは"

msgid "Current time: {time}"
msgstr "現在の日時: {time}"

データ連携

GraphQL APIの使用

Dashboard拡張では、@vendure/dashboardが提供するapiオブジェクトを使用してGraphQL APIにアクセスします。

import { api } from '@/vdb/graphql/api';

const query = /* GraphQL */ `
  query GetCustomers($options: CustomerListOptions) {
    customers(options: $options) {
      items {
        id
        emailAddress
        firstName
        lastName
      }
      totalItems
    }
  }
`;

const result = await api.query(query, { options: { take: 10 } });

React Queryの使用

Dashboard拡張では、React Queryを使用してデータフェッチを管理します。

import { useQuery, useMutation } from '@tanstack/react-query';
import { api } from '@/vdb/graphql/api';

export function MyComponent() {
  const { data, isLoading } = useQuery({
    queryKey: ['customers'],
    queryFn: () => api.query(customersQuery, {}),
  });

  const { mutateAsync } = useMutation({
    mutationFn: variables => api.mutate(updateCustomerMutation, variables),
  });

  // ...
}

共通ユーティリティ

ファイルダウンロードヘルパー

Dashboard拡張でファイルダウンロード機能(CSVエクスポート、PDF生成等)を実装する際は、共通ユーティリティ関数を使用すること。

場所: apps/vendure-server/src/plugins/ritsubi-admin-extensions/dashboard/utils/download-helper.ts

提供関数:

  • downloadBlob(blob: Blob, filename: string): Blobデータをダウンロード
  • downloadBase64(base64: string, filename: string, mimeType: string): Base64文字列をデコードしてダウンロード
  • downloadText(content: string, filename: string, mimeType?: string): テキストデータをダウンロード

使用例:

import { downloadBase64, downloadText } from './utils/download-helper.js';

// GraphQLからBase64エンコードされたCSVデータを受け取る場合
const response = await api.mutate(exportMutation, variables);
downloadBase64(
  response.exportSmileOrders.csvData,
  response.exportSmileOrders.filename,
  'text/csv'
);

// テキストデータ(CSV文字列など)を直接ダウンロードする場合
const csv = toCsv([header, ...rows]);
downloadText(csv, 'export.csv', 'text/csv');

重要な原則:

  • DOM操作の一元化: document.createElement('a')document.body.appendChild() などのDOM操作は、このヘルパー関数内に集約されている。コンポーネント内で直接DOM操作を行わないこと。
  • Vendure標準の不在: VendureのDashboardライブラリには、ファイルダウンロード用の標準ユーティリティが提供されていないため、プロジェクト固有の実装としてこのヘルパーを使用する。
  • ブラウザ互換性: Firefox等の互換性のために document.body.appendChild を使用し、確実にクリーンアップ(removeChildURL.revokeObjectURL)を行う実装となっている。

実装の背景:

Vendureはヘッドレスコマースフレームワークとして、フロントエンドの実装方法に対して中立な立場を取るため、特定のブラウザAPI(DOM操作など)に依存するヘルパーをCoreに含めない方針です。そのため、ファイルダウンロード機能が必要な場合は、プロジェクト固有のユーティリティとして実装する必要があります。

テスト戦略

6.2 Vitestによるテスト

Vendure v3プロジェクトでは、高速なテストランナーであるVitestが推奨されています。Jestの代わりにVitestを使用することで、設定の簡素化と実行速度の向上が見込めます。

テストの種類

  1. 単体テスト (.spec.ts): サービスやヘルパー関数のロジックを検証します。DB接続は行わず、依存関係はモック化します。

  2. E2Eテスト (.e2e-spec.ts): 実際のデータベースを使用して、APIエンドポイント(GraphQL)からデータベースまでのフロー全体を検証します。@vendure/testingパッケージのcreateTestEnvironmentを使用します。

Vitest設定例

vitest.config.tsを作成し、NestJSのデコレータをサポートするためにunplugin-swcプラグインを使用します。

import { defineConfig } from 'vitest/config';
import swc from 'unplugin-swc';

export default defineConfig({
  plugins: [
    swc.vite({
      jsc: {
        transform: {
          useDefineForClassFields: false,
          legacyDecorator: true,
          decoratorMetadata: true,
        },
      },
    }),
  ],
  test: {
    globals: true,
    environment: 'node',
  },
});

テスト実行コマンド

# 全テスト実行
pnpm test

# UIモードで実行
pnpm run test:ui

# カバレッジ計測
pnpm run test:coverage

# 関連テストの実行(変更されたファイルのみ)
pnpm run test:related src/my-changed-file.ts

6.3 統合テスト基盤(バックエンド)

バックエンド統合テスト環境が apps/vendure-server/test/integration 配下に整備されています。 SQLite(インメモリ)を使用し、実際のVendureサーバーを起動してGraphQL API経由でテストを実行します。

セットアップの特徴

  • DB: Postgres/PGroonga(docker compose dev)を使用
  • Initializer: InMemorySqljsInitializer により、テストごとに分離不要な単一DB構成
  • Client: TestGraphQLClient により、node-fetch ベースの安定したGraphQLリクエストが可能(認証トークン自動管理)

実行方法

# Vendureサーバーの統合テストのみ実行
pnpm --filter ritsubi-vendure-server test:integration

# ルートから実行(Turbo)
pnpm test:integration

テストの作成方法

test/integration/setup/test-environment.ts から createTestEnv をインポートして使用します。

import { createTestEnv, TestServer } from './setup/test-environment';
import { TestGraphQLClient } from './helpers/graphql-client';
import { initialData } from './fixtures/initial-data';
import gql from 'graphql-tag';

describe('My Feature', () => {
  let server: TestServer;
  let adminClient: TestGraphQLClient;

  beforeAll(async () => {
    const env = await createTestEnv();
    server = env.server;
    await server.init({ initialData, logging: false });

    // クライアントの初期化(ポート動的取得)
    const port = server.app.getHttpServer().address().port;
    adminClient = new TestGraphQLClient(`http://localhost:${port}/admin-api`);
    await adminClient.asSuperAdmin();
  });

  afterAll(async () => {
    await server.destroy();
  });

  it('should work', async () => {
    const result = await adminClient.query(gql`...`);
    expect(result).toBeDefined();
  });
});

参照リンク

プロジェクト内ドキュメント

公式ドキュメント


更新日: 2025-01-XX
メンテナー: Ritsubi開発チーム