🧪

Claude Code のSKILLでテスト自動化を実現:既存パターン学習とリファクタリングでカバレッジ向上

に公開

はじめに

テスト作成は重要ですが、時間がかかる作業です。特に以下のような課題があります:

  • ⏱️ 時間がかかる: 1ファイルのテスト作成に30分〜1時間
  • 🎯 試行錯誤が多い: モック戦略が定まらず、何度も書き直し
  • 📐 一貫性がない: ファイルごとにテストの書き方が異なる
  • 🤔 テスタビリティ判断が難しい: テストを書く前にリファクタリングすべきか判断できない

私たちが開発している「Delivroute」は、Google Maps APIを活用した配送ルート最適化SaaSです。最大10地点の配送ルートを自動で最適化し、配送効率を向上させることを目的としています。

このプロジェクトでは、Claude Code のカスタムスキルを3つ実装し、テスト自動化の課題を解決しました。

実績:

  • 📈 カバレッジ 27% → 35% (Phase H完了時)
  • 🧪 88テスト追加 (Phase H のみ)
  • ⏱️ テスト作成時間 50%削減
  • ✅ 重大バグ1件発見・修正

本記事では、テスト自動化を実現した3つのスキルの仕組みと連携方法を解説します。

3つのスキルの概要

テスト自動化を実現するために、以下の3つのスキルを実装しました:

1. create-test: 単一ファイルのテスト作成

役割: 単一ファイルの単体テスト作成に特化

特徴:

  • 📚 既存テストパターンの自動学習: 同じディレクトリや外部依存のテストを自動参照
  • 🎯 モック戦略の推論: localStorage, Date, fetch等のモック方法を既存パターンから推論
  • 🧪 AAA パターンの自動適用: Arrange, Act, Assert の構造を自動適用
  • 📊 カバレッジ目標 90%以上: 意味のあるテストを追加

2. test-expansion: 複数ファイルのテスト拡充

役割: 複数ファイルのテスト拡充を戦略的に管理

特徴:

  • 📊 カバレッジレポート分析: 未テストファイルを自動抽出
  • 🚦 テスタビリティ評価: 緑・黄・赤の3段階でテストしやすさを評価
  • 🔄 create-test の呼び出し: 各ファイルに対して create-test を自動実行
  • 📈 カバレッジ集計: 全体のカバレッジ向上を可視化

3. refactor-for-testability: リファクタリング

役割: テストしにくいコードをテストしやすい構造にリファクタリング

特徴:

  • Pure関数の抽出(最優先): 副作用なし、モック不要でテスト容易
  • 🔗 疎結合化(依存性注入): 外部依存をモック可能に
  • 🤖 create-test の自動呼び出し: リファクタリング完了後、自動的にテスト追加
  • 📈 カバレッジ大幅向上: 20% → 85% の実績

スキルの2層構造

3つのスキルは以下のような関係になっています:

test-expansion(戦略層)
  ├── カバレッジレポート分析
  ├── テスタビリティ評価
  │   ├── ✅ 緑(高)→ create-test 呼び出し
  │   ├── ⚠️ 黄(中)→ ユーザー選択
  │   └── ❌ 赤(低)→ refactor-for-testability 推奨
  └── カバレッジ集計・報告

refactor-for-testability(リファクタリング層)
  ├── Pure関数の抽出
  ├── 疎結合化
  └── create-test 自動呼び出し

create-test(実行層)
  ├── 既存テストパターン参照
  ├── テスト設計
  ├── テスト実装
  └── テスト実行・検証

create-test スキル:単一ファイルのテスト作成

既存テストパターンの自動学習

create-test スキルの最大の特徴は、既存テストパターンを自動的に参照し、一貫性の高いテストを作成することです。

Phase 2: 既存テストパターンの参照

// 1. 同じディレクトリ内のテストファイルを検索
// 例: lib/cache/cache.ts の場合
// lib/cache/ 配下の *.test.ts ファイルを検索

// 2. 同じ外部依存を使っているテストを検索
// 例: localStorage を使う場合
grep -r "jest-localstorage-mock" lib --include="*.test.ts"

// 既存パターンから以下を学習:
// - describe/it の構造
// - beforeEach/afterEach の使い方
// - AAA パターンの実装
// - テストケースの命名規則(日本語 or 英語)

実装例:localStorage + Date.now() のモック

キャッシュ機能のテスト作成時に、以下のパターンを学習しました:

cache.test.ts
import 'jest-localstorage-mock'; // localStorage モック

// グローバルモックを解除(必要な場合)
jest.unmock('@/lib/cache');

import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
import { CacheManager } from './cache';

let dateNowSpy: jest.SpyInstance;

beforeEach(() => {
  // localStorage をクリア
  localStorage.clear();

  // 全モックをクリア
  jest.clearAllMocks();

  // console.error をモック化(エラーログ抑制)
  jest.spyOn(console, 'error').mockImplementation(() => {});

  // Date.now をモック化して時間を固定
  dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(1000000);
});

afterEach(() => {
  // 全モックをリストア
  jest.restoreAllMocks();
});

describe('CacheManager', () => {
  let cache: CacheManager;

  beforeEach(() => {
    cache = new CacheManager('app_cache');
  });

  describe('set', () => {
    it('データを正しく保存できる', () => {
      // Arrange: テストデータの準備
      const key = 'user_123';
      const data = { name: 'John Doe', email: 'john@example.com' };
      const now = 1000000;
      dateNowSpy.mockReturnValue(now);

      // Act: テスト対象の実行
      cache.set(key, data);

      // Assert: 結果の検証
      const stored = JSON.parse(localStorage.getItem('app_cache:user_123')!);
      expect(stored.data).toEqual(data);
      expect(stored.timestamp).toBe(now);
    });
  });

  // 他のテストケース...
});

学習内容:

  1. jest-localstorage-mock をインポート
  2. jest.unmock() でグローバルモックを解除
  3. Date.now() をモック化して時間を固定
  4. console.error をモック化してログ抑制
  5. ✅ beforeEach/afterEach で状態リセット
  6. ✅ AAA パターン(Arrange, Act, Assert)遵守
  7. ✅ 日本語のテストケース名

実行結果

$ pnpm test cache.test.ts

PASS  cache.test.ts
  CacheManager
    constructor
      ✓ デフォルトプレフィックスで初期化できる (3 ms)
      ✓ カスタムプレフィックスで初期化できる (1 ms)
    get
      ✓ 保存されたデータを取得できる (2 ms)
      ✓ 期限切れのデータはnullを返す (1 ms)
      ✓ 不正なJSONデータはnullを返す (1 ms)
      ✓ 存在しないキーはnullを返す (1 ms)
    set
      ✓ デフォルトTTLでデータを保存できる (2 ms)
      ✓ カスタムTTLでデータを保存できる (1 ms)
      ✓ LocalStorage満杯時にclearExpiredを呼び出す (3 ms)
    remove
      ✓ データを削除できる (1 ms)
      ✓ 存在しないキーの削除はエラーにならない (1 ms)
    clearExpired
      ✓ 期限切れのデータのみ削除される (2 ms)
      ✓ 他のプレフィックスのデータは削除されない (1 ms)
      ✓ パースエラーのデータは削除される (1 ms)
    clearAll
      ✓ 全データを削除できる (1 ms)
      ✓ 他のプレフィックスのデータは削除されない (1 ms)

Test Suites: 1 passed, 1 total
Tests:       16 passed, 16 total
Snapshots:   0 total
Time:        0.5 s

----------|---------|----------|---------|---------|
File      | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
cache.ts  |   82.75 |    72.72 |   85.71 |   82.75 |
----------|---------|----------|---------|---------|

成果:

  • ✅ 16テストケース
  • ✅ カバレッジ 82.75%
  • ✅ 全テストパス
  • ⏱️ 作成時間 15分(手動なら30-40分)

test-expansion スキル:複数ファイルのテスト拡充

テスタビリティ評価(緑・黄・赤)

test-expansion スキルの重要な機能が、テストを書く前にテスタビリティを評価することです。

評価基準

以下の5つの項目でテスタビリティを評価:

  1. 関数の長さ

    • ✅ 50行未満 → そのままテスト可能
    • ⚠️ 50-100行 → 関数分割を検討
    • ❌ 100行以上 → リファクタリング推奨
  2. 外部依存の数

    • ✅ 0-1つ → そのままテスト可能
    • ⚠️ 2-3つ → 依存性注入を検討
    • ❌ 4つ以上 → リファクタリング必須
  3. 副作用の複雑さ

    • ✅ 副作用なし(Pure関数) → テスト容易
    • ⚠️ 1種類の副作用のみ → モックで対応可能
    • ❌ DB操作、API呼び出し、ファイルIOが混在 → Pure関数に分離
  4. 分岐の複雑さ(Cyclomatic Complexity)

    • ✅ 分岐が4個以下 → そのままテスト可能
    • ⚠️ 分岐が5-9個 → 分割を検討
    • ❌ 分岐が10個以上 → 関数分割必須
  5. ハードコードされた値

    • ✅ ハードコードなし → そのままテスト可能
    • ⚠️ 一部ハードコード → 改善余地あり
    • ❌ API URL, タイムアウト値等がハードコード → 引数化・設定ファイル化

テスタビリティ判定

評価結果に基づいて、以下のように判定:

  • A: テスタビリティ高(緑) → そのままテスト追加
  • B: テスタビリティ中(黄) → ユーザー選択(簡単なリファクタリング or そのままテスト追加)
  • C: テスタビリティ低(赤) → リファクタリング必須(refactor-for-testability 推奨)

実装例:lib/ 配下の3ファイル

以下のようなワークフローでテスト拡充を実施できます:

# ユーザー: lib/ 配下でカバレッジが低いファイルのテストを追加
$ /test-expansion lib/

# Phase 1: テスト対象の選定
# カバレッジレポート分析 → 未テストファイル5つ検出
# - lib/validation/rules.ts (0%)
# - lib/utils/formatter.ts (15%)
# - lib/session/handler.ts (0%)
# - lib/pricing/calculator.ts (20%)
# - lib/data/sorter.ts (10%)
#
# 優先度順にリストアップ → ユーザーに提示
# ユーザー選択 → 上位3ファイル

# Phase 2: テスタビリティ評価
# - lib/validation/rules.ts: ✅ テスタビリティ高(緑)
# - lib/utils/formatter.ts: ✅ テスタビリティ高(緑)
# - lib/session/handler.ts: ⚠️ テスタビリティ中(黄)
#   → ユーザー選択: そのまま create-test 実行

# Phase 3: create-test 呼び出し
# `/create-test lib/validation/rules.ts` → 20/20 pass, 100%カバレッジ ✅
# `/create-test lib/utils/formatter.ts` → 15/15 pass, 92%カバレッジ ✅
# `/create-test lib/session/handler.ts` → 18/18 pass, 85%カバレッジ ✅

# Phase 4: カバレッジ集計
# 全体カバレッジ: 35.2% → 42.8% (+7.6%)
# 追加テスト数: 53テスト

# Phase 5: ドキュメント更新
# unit-test-strategy.md 更新
# 完了報告

成果:

  • ✅ 53テストケース追加
  • ✅ カバレッジ +7.6%
  • ⏱️ 作成時間 1時間(手動なら3-4時間)

refactor-for-testability スキル:リファクタリング

Pure関数の抽出(最優先)

refactor-for-testability スキルの最重要原則は、Pure関数化を最優先することです。

Pure関数は以下の特徴があります:

  • 🧪 同じ入力 → 同じ出力
  • 🚫 副作用なし(API呼び出し、DB操作、ファイルIO等なし)
  • モック不要でテスト可能
  • 🔄 再利用性が高い

Before/After コード例

テストしにくいコードからテストしやすいコードへのリファクタリング例を紹介します。

Before: テストが難しいコード

order-processor.ts(リファクタリング前)
async function processOrder(orderId: string) {
  // データ取得(30行)
  const order = await database.query('SELECT * FROM orders WHERE id = ?', [orderId]);
  const user = await database.query('SELECT * FROM users WHERE id = ?', [order.userId]);

  // バリデーション(20行)
  if (!order) throw new Error('Order not found');
  if (!user) throw new Error('User not found');
  if (order.status !== 'pending') throw new Error('Order already processed');

  // 価格計算(30行)
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.quantity;
  }
  const tax = total * 0.1;
  const finalPrice = total + tax;

  // 決済(20行)
  await paymentClient.charge({
    amount: finalPrice,
    currency: 'usd',
    customerId: user.paymentCustomerId,
  });

  // 保存(10行)
  await database.execute(
    'UPDATE orders SET status = ?, total = ? WHERE id = ?',
    ['completed', finalPrice, orderId]
  );
}

問題点:

  • ❌ 関数の長さ: 110行
  • ❌ 外部依存: 3つ(Database, PaymentClient, API)
  • ❌ 副作用の混在: DB操作、API呼び出し、計算ロジックが混在
  • ❌ Cyclomatic Complexity: 8
  • ❌ テスタビリティ: 低(赤)

After: テストしやすいコード

order-processor.ts(リファクタリング後)
// ✅ Pure関数(テスト容易、モック不要)
function calculateTotal(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

function calculateTax(total: number): number {
  return total * 0.1;
}

function calculateFinalPrice(total: number, tax: number): number {
  return total + tax;
}

// ✅ Pure関数(バリデーション)
function validateOrder(order: Order | null, user: User | null): void {
  if (!order) throw new Error('Order not found');
  if (!user) throw new Error('User not found');
  if (order.status !== 'pending') throw new Error('Order already processed');
}

// ✅ 疎結合化(依存性注入)
interface OrderRepository {
  getOrder(orderId: string): Promise<Order | null>;
  updateOrder(orderId: string, data: Partial<Order>): Promise<void>;
}

interface UserRepository {
  getUser(userId: string): Promise<User | null>;
}

interface PaymentService {
  charge(customerId: string, amount: number): Promise<void>;
}

// メイン関数(統合)
async function processOrder(
  orderId: string,
  orderRepo: OrderRepository,
  userRepo: UserRepository,
  paymentService: PaymentService
) {
  // データ取得
  const order = await orderRepo.getOrder(orderId);
  const user = await userRepo.getUser(order.userId);

  // バリデーション(Pure関数)
  validateOrder(order, user);

  // 価格計算(Pure関数)
  const total = calculateTotal(order.items);
  const tax = calculateTax(total);
  const finalPrice = calculateFinalPrice(total, tax);

  // 決済
  await paymentService.charge(user.paymentCustomerId, finalPrice);

  // 保存
  await orderRepo.updateOrder(orderId, { status: 'completed', total: finalPrice });
}

改善点:

  • ✅ 関数の長さ: 各関数10-20行
  • ✅ Pure関数の割合: 60%(calculateTotal, calculateTax, calculateFinalPrice, validateOrder)
  • ✅ 疎結合化: 依存性注入でテスト時にモック注入可能
  • ✅ Cyclomatic Complexity: 各関数1-3
  • ✅ テスタビリティ: 高(緑)

create-test の自動呼び出し

リファクタリング完了後、自動的に create-test スキルを呼び出してテストを追加します。

order-processor.test.ts(自動生成)
describe('calculateTotal (Pure関数)', () => {
  it('アイテムの合計金額を正しく計算できる', () => {
    // Arrange
    const items = [
      { price: 100, quantity: 2 },
      { price: 200, quantity: 1 },
    ];

    // Act
    const result = calculateTotal(items);

    // Assert
    expect(result).toBe(400); // 100*2 + 200*1 = 400
  });

  it('空配列の場合は0を返す', () => {
    expect(calculateTotal([])).toBe(0);
  });
});

describe('calculateTax (Pure関数)', () => {
  it('税金を正しく計算できる', () => {
    expect(calculateTax(1000)).toBe(100); // 1000 * 0.1 = 100
  });
});

describe('validateOrder (Pure関数)', () => {
  it('正常なorderとuserの場合はエラーをスローしない', () => {
    const order = { status: 'pending', items: [], userId: '123' };
    const user = { paymentCustomerId: 'cust_123' };

    expect(() => validateOrder(order, user)).not.toThrow();
  });

  it('orderがnullの場合はエラーをスロー', () => {
    expect(() => validateOrder(null, {})).toThrow('Order not found');
  });

  it('orderのstatusがpending以外の場合はエラーをスロー', () => {
    const order = { status: 'completed', items: [], userId: '123' };
    const user = { paymentCustomerId: 'cust_123' };

    expect(() => validateOrder(order, user)).toThrow('Order already processed');
  });
});

// メイン関数のテスト(モックを使用)
describe('processOrder', () => {
  let mockOrderRepo: OrderRepository;
  let mockUserRepo: UserRepository;
  let mockPaymentService: PaymentService;

  beforeEach(() => {
    mockOrderRepo = {
      getOrder: jest.fn(),
      updateOrder: jest.fn(),
    };
    mockUserRepo = {
      getUser: jest.fn(),
    };
    mockPaymentService = {
      charge: jest.fn(),
    };
  });

  it('正常なorderの場合は処理が完了する', async () => {
    // Arrange
    const order = {
      userId: 'user_123',
      status: 'pending',
      items: [{ price: 100, quantity: 2 }],
    };
    const user = { paymentCustomerId: 'cust_123' };

    (mockOrderRepo.getOrder as jest.Mock).mockResolvedValue(order);
    (mockUserRepo.getUser as jest.Mock).mockResolvedValue(user);

    // Act
    await processOrder('order_123', mockOrderRepo, mockUserRepo, mockPaymentService);

    // Assert
    expect(mockPaymentService.charge).toHaveBeenCalledWith('cust_123', 220); // 200 + 20(税)
    expect(mockOrderRepo.updateOrder).toHaveBeenCalledWith('order_123', {
      status: 'completed',
      total: 220,
    });
  });
});

成果:

  • ✅ カバレッジ: 30% → 85% (+55%)
  • ✅ Pure関数はモック不要で簡単にテスト可能
  • ✅ 副作用部分のみモック使用(疎結合化のおかげ)
  • ⏱️ リファクタリング + テスト作成: 1時間(手動なら3-4時間)

3スキル連携の実践例

test-expansion → refactor-for-testability → create-test

3つのスキルが連携して動作する実例を紹介します。

# ユーザー: lib/business-logic.ts のテストを追加
$ /test-expansion lib/business-logic.ts

# Phase 1: テスタビリティ評価
# - ファイル読み込み → 150行、複雑度15
# - 評価: ❌ テスタビリティ低(赤)
#   - 関数の長さ: ❌ 150行(推奨: 50行未満)
#   - 外部依存: ❌ 5つのAPI呼び出し(推奨: 1つ以下)
#   - 分岐: ❌ 12個(推奨: 4個以下)
# - 推奨: リファクタリング必須

# Phase 2: ユーザー承認
# → リファクタリングを実施

# Phase 3: refactor-for-testability 実行
$ /refactor-for-testability lib/business-logic.ts

# - Phase 1: コード分析
# - Phase 2: リファクタリング戦略
#   - Pure関数抽出: 3つの関数に分離
#   - 疎結合化: 依存性注入でモック可能に
# - Phase 3: リファクタリング実装
#   - Pure関数抽出完了 → 既存テスト確認 ✅
#   - 疎結合化完了 → 既存テスト確認 ✅
# - Phase 4: create-test 自動呼び出し
#   - カバレッジ: 20% → 85% ✅

# Phase 4: test-expansion に戻る
# - カバレッジ集計・報告
# - unit-test-strategy.md 更新

# 完了報告
# ✅ リファクタリング完了 + テスト追加完了

実際の効果(実プロジェクトの実績)

実際のプロジェクトでは、Business-Critical Tests フェーズで以下の成果を上げました:

対象ファイル:

  • lib/config/plans.ts(サービスプラン設定)
  • lib/subscription/renewal.ts(サブスクリプション更新処理)
  • lib/utils/date.ts(日付計算ユーティリティ)

実施内容:

  1. config/plans.test.ts の作成

    • 36テスト
    • カバレッジ: 23.8% → 92.85% (+69.05%)
  2. subscription/renewal.test.ts の作成

    • 29テスト
    • カバレッジ: 0% → 97.05% (+97.05%)
    • 重大バグ発見・修正: 日付計算バグ(月末日 + 1ヶ月の計算が不正確)
  3. utils/date.ts のリファクタリング・テスト作成

    • 23テスト
    • カバレッジ: 100%
    • リファクタリング効果: renewal.ts の日付計算ロジック 19行 → 3行に簡略化

成果サマリー:

  • ✅ 全体カバレッジ: 27.24% → 32.22% (+4.98%)
  • ✅ テスト総数: 340 → 428 (+88テスト)
  • ✅ lib/subscriptionカバレッジ: 38.93% → 98.21% (+59.28%)
  • ✅ 重大バグ修正: 1件(サブスクリプション更新日計算)
  • ✅ コード品質向上: Pure関数抽出、テスト容易性向上

スキルの作り方(Claude Code)

ディレクトリ構成

Claude Code のカスタムスキルは、以下のディレクトリ構成で管理します:

.claude/
├── skills/                    # スキルディレクトリ
│   ├── create-test/
│   │   └── SKILL.md          # スキル定義(615行)
│   ├── test-expansion/
│   │   └── SKILL.md          # スキル定義(483行)
│   ├── refactor-for-testability/
│   │   └── SKILL.md          # スキル定義(917行)
│   └── README.md             # スキル一覧
└── commands/                  # スラッシュコマンド
    ├── create-test.md
    ├── test-expansion.md
    └── refactor-for-testability.md

SKILL.md の書き方

スキルファイル(SKILL.md)は、以下の構成で記述します:

---
name: create-test
description: 単一ファイルの単体テスト作成に特化したスキル - 既存テストパターンを自動学習し、一貫性の高いテストを効率的に作成
---

# create-test

## Instructions

単一ファイルの単体テスト作成に特化したスキルです。既存のテストパターンを自動的に参照し、モック戦略を学習することで、試行錯誤を削減し、一貫性の高いテストを効率的に作成します。

---

## 実行フロー

### Phase 1: 対象コードの分析

1. ファイルを読み込む
2. 依存関係の確認
3. テスト対象の分類

### Phase 2: 既存テストパターンの参照

1. 同ディレクトリ内のテストファイル検索
2. 同じ外部依存を使っているテストの検索
3. モック戦略を推論

### Phase 3: テスト設計

1. テストケース設計(正常系、異常系、境界値、エッジケース)
2. テストケース数の見積もり
3. モック戦略の決定
4. テスト計画書の作成 → ユーザー承認

### Phase 4: テスト実装

1. テストファイルの作成
2. モックの実装
3. AAA パターンの遵守
4. テストケースの命名規則

### Phase 5: テスト実行・検証

1. テスト実行
2. カバレッジ確認
3. ドキュメント更新

---

## 重要な原則

1. 既存パターンを最優先で参照
2. テスト設計を先に行う
3. カバレッジは手段であって目的ではない
4. モックは最小限に
5. テストは読みやすく
6. デバッグ時間を削減

---

## 実行例

(省略)

スラッシュコマンドの登録

スラッシュコマンド(.claude/commands/create-test.md)は、以下のように記述します:

`create-test` スキルを実行してください。

単一ファイルの単体テスト作成に特化したスキルです。既存のテストパターンを自動的に参照し、モック戦略を学習することで、試行錯誤を削減し、一貫性の高いテストを効率的に作成します。

**引数**: {{ARGUMENTS}}

これにより、以下のように実行できます:

/create-test lib/cache/storage.ts

まとめ

本記事では、Claude Code のカスタムスキルを3つ実装し、テスト自動化を実現した事例を紹介しました。

テスト自動化の効果

  • テスト作成時間 50%削減: 既存パターン学習により試行錯誤が減少
  • カバレッジ +5%向上: Pure関数化により効率的にカバレッジ向上
  • 重大バグ発見: テスト作成中にビジネスロジックのバグを発見・修正
  • テストの一貫性向上: モック戦略が統一され、メンテナンス性向上

3スキル連携の価値

  1. create-test: 既存パターン学習により、一貫性の高いテストを効率的に作成
  2. test-expansion: テスタビリティ評価により、無理にテストを書くのではなく、まずリファクタリング
  3. refactor-for-testability: Pure関数化 + 疎結合化により、テストしやすいコードに変換

Delivroute について

Delivrouteは、配送ルート最適化を簡単に行えるWebアプリケーションです。

  • 🗺️ 最大10地点のルート最適化: Google Maps Routes APIで最適な配送順序を自動計算
  • 簡単CSVインポート: CSVファイルから一括で配送先を登録
  • 💰 クレジット制: 無料プラン(月3回)、Proプラン(月60回)で気軽に利用可能

👉 サービスはこちら: https://delivroute.com


参考資料

Discussion