T-CREATOR

Codex が誤ったコードを出すときの対処:再現最小化・拘束条件・テスト駆動の適用

Codex が誤ったコードを出すときの対処:再現最小化・拘束条件・テスト駆動の適用

AI コーディングアシスタントの Codex や GitHub Copilot を使っていると、期待通りのコードが生成されず、むしろバグを含んだコードが出力されてしまうことがありますよね。

せっかく効率化のために導入したのに、かえって修正に時間がかかってしまう…そんな経験をお持ちの方も多いのではないでしょうか。本記事では、Codex が誤ったコードを出力する際に有効な 3 つの実践的なアプローチを解説します。具体的には「再現最小化」「拘束条件の明示」「テスト駆動開発の適用」という手法を、実際のコード例とともにご紹介していきますね。

背景

AI コーディングアシスタントの普及と課題

近年、OpenAI が開発した Codex をベースにした GitHub Copilot をはじめ、多くの AI コーディングアシスタントが登場しています。これらのツールは、プログラマーの生産性を大幅に向上させる可能性を秘めていますが、完璧ではありません。

特に複雑なロジックや、コンテキストが十分に伝わっていない場合、誤ったコードを生成してしまうケースが少なくないのです。

Codex の動作原理

Codex は大規模な言語モデル(Large Language Model, LLM)を基盤としており、膨大なコードデータから学習しています。プロンプト(指示文やコメント)を受け取ると、統計的に「もっともらしい」コードを生成する仕組みです。

ただし、あくまで確率的な予測であるため、以下のような状況では誤ったコードを出力する可能性があります。

  • プロンプトが曖昧で意図が伝わらない
  • 複雑な仕様や制約条件が考慮されていない
  • エッジケースやエラーハンドリングが不十分
  • 学習データに含まれていないパターンやライブラリを扱う

以下の図は、Codex がプロンプトからコードを生成する基本的な流れを示しています。

mermaidflowchart LR
  developer["開発者"] -->|プロンプト入力| codex["Codex<br/>(LLMエンジン)"]
  codex -->|統計的予測| code["生成コード"]
  code -->|検証| developer
  developer -->|修正指示| codex

図で理解できる要点

  • 開発者がプロンプトを入力すると、Codex が統計的にコードを予測生成します
  • 生成されたコードは開発者が検証し、必要に応じて修正指示を出すループが発生します

Codex が誤ったコードを出す典型的なシナリオ

実際の開発現場では、次のようなシナリオで Codex が期待外れのコードを生成することがあります。

#シナリオ具体例
1プロンプトの曖昧さ「データを取得する関数」だけでは、どのデータを、どこから、どのように取得するのか不明
2複雑な業務ロジックドメイン固有のルールや制約が伝わらず、一般的なパターンのみが生成される
3エッジケースの考慮不足null チェックや境界値処理が抜け落ちる
4最新ライブラリへの対応遅れ学習データに含まれていない最新 API の誤用

これらの問題を解決するために、本記事では 3 つの実践的アプローチを提案します。

課題

Codex が生成するコードの品質問題

Codex が誤ったコードを出力すると、開発効率の低下だけでなく、本番環境でのバグやセキュリティリスクにつながる可能性もあります。特に以下のような課題が顕在化しています。

課題 1:問題の特定に時間がかかる

生成されたコードが長く複雑な場合、どの部分が誤っているのかを特定するのに時間がかかります。エラーメッセージが出ない場合や、ロジックエラーの場合はさらに困難です。

typescript// Codex が生成した複雑なコード例(問題箇所が不明瞭)
async function processUserData(userId: string) {
  const user = await fetchUser(userId);
  const orders = await fetchOrders(user.id);
  const recommendations = await getRecommendations(user);
  const analytics = processAnalytics(
    orders,
    recommendations
  );
  // どこかでエラーが出るが、特定が困難...
  return formatResponse(
    user,
    orders,
    recommendations,
    analytics
  );
}

上記のコードは一見正しそうに見えますが、実際には fetchOrdersgetRecommendations でエラーハンドリングが不足していたり、user.id が undefined の可能性があったりします。

課題 2:制約条件が伝わらない

プロンプトで「ユーザー情報を取得する」と指示しても、以下のような制約が Codex に伝わっていなければ、不適切なコードが生成されます。

  • データベースは PostgreSQL を使用
  • ユーザーが存在しない場合は 404 エラーを返す
  • メールアドレスは必ずマスキングする
  • パフォーマンスのため、クエリは 1 回に抑える

こうした制約条件を明示しないと、Codex は一般的なパターンしか生成できません。

課題 3:テストがない状態での検証困難

Codex が生成したコードをテストなしで検証しようとすると、手動で様々なケースを試す必要があり、非効率です。また、後からバグが見つかった場合、修正の影響範囲が不明確になりがちです。

以下の図は、Codex が誤ったコードを出す際の問題の連鎖を示しています。

mermaidflowchart TD
  ambiguous["曖昧なプロンプト"] --> wrong["誤ったコード生成"]
  wrong --> long["複雑で長いコード"]
  long --> hard["問題特定が困難"]
  hard --> manual["手動検証に時間"]
  manual --> bug["バグの見逃し"]
  bug --> production["本番環境での障害"]

  style production fill:#ffcccc
  style ambiguous fill:#fff4cc

図で理解できる要点

  • 曖昧なプロンプトが起点となり、誤ったコード生成の連鎖が始まります
  • 最終的に本番環境での障害につながるリスクが高まります
  • この負のサイクルを断ち切るための対策が必要です

解決策

これらの課題を解決するため、3 つの実践的アプローチを組み合わせて使用することが効果的です。それぞれの手法を詳しく見ていきましょう。

解決策 1:再現最小化(Minimal Reproducible Example)

再現最小化とは、問題を最小限のコードに絞り込むアプローチです。複雑な処理を一度に Codex に生成させるのではなく、小さな単位に分割して、1 つずつ検証しながら進めます。

再現最小化の手順

#ステップ説明
1問題の切り分け大きな機能を小さな関数に分割する
2最小単位でのテスト各関数が正しく動作するか確認
3段階的な統合動作確認した関数を組み合わせていく
4エラー箇所の特定問題が発生した最小単位を特定

実践例:複雑な処理を分割する

まず、悪い例として、すべてを一度に Codex に生成させるケースを見てみましょう。

typescript// 悪い例:すべてを一度に生成させる
// プロンプト: "ユーザーデータを取得して、注文履歴と推奨商品を含めて整形して返す関数を作成"

// Codex が生成したコード(問題箇所の特定が困難)
async function getUserWithRecommendations(userId: string) {
  // この中に複数の処理が混在し、どこでエラーが出るか不明
  const user = await db.query(
    'SELECT * FROM users WHERE id = $1',
    [userId]
  );
  const orders = await db.query(
    'SELECT * FROM orders WHERE user_id = $1',
    [userId]
  );
  const recommendations = await analyzeAndRecommend(
    user,
    orders
  );
  return { user, orders, recommendations };
}

このアプローチでは、エラーが発生した際にどの部分が問題なのか特定が困難です。

次に、良い例として再現最小化を適用したケースを見てみます。

typescript// 良い例:最小単位に分割する
// ステップ1: ユーザー取得のみを実装

// プロンプト: "指定されたIDのユーザーを1件取得する関数を作成。存在しない場合はnullを返す"
async function fetchUserById(
  userId: string
): Promise<User | null> {
  const result = await db.query(
    'SELECT * FROM users WHERE id = $1',
    [userId]
  );

  // 結果が空の場合はnullを返す
  return result.rows[0] || null;
}

ユーザー取得処理だけを最小単位として実装することで、この部分の動作を確実に検証できます。

typescript// ステップ2: 注文履歴取得を実装

// プロンプト: "指定されたユーザーIDの注文履歴を全件取得する関数"
async function fetchOrdersByUserId(
  userId: string
): Promise<Order[]> {
  const result = await db.query(
    'SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC',
    [userId]
  );

  return result.rows;
}

注文履歴取得も独立した関数として実装し、単体でテストできるようにします。

typescript// ステップ3: 推奨商品の生成を実装

// プロンプト: "ユーザーと注文履歴から推奨商品を生成する関数"
function generateRecommendations(
  user: User,
  orders: Order[]
): Recommendation[] {
  // シンプルなロジックから始める
  const purchasedCategories = orders.map(
    (order) => order.category
  );
  const uniqueCategories = [
    ...new Set(purchasedCategories),
  ];

  return uniqueCategories.map((category) => ({
    category,
    reason: `過去に${category}カテゴリの商品を購入済み`,
  }));
}

推奨商品生成は純粋関数として実装し、データベースアクセスと切り離しています。

typescript// ステップ4: すべてを統合する

async function getUserWithRecommendations(userId: string) {
  // 検証済みの関数を組み合わせる
  const user = await fetchUserById(userId);

  // ユーザーが存在しない場合の処理
  if (!user) {
    throw new Error(`User not found: ${userId}`);
  }

  const orders = await fetchOrdersByUserId(userId);
  const recommendations = generateRecommendations(
    user,
    orders
  );

  return { user, orders, recommendations };
}

各部分が検証済みであるため、統合時の問題特定が容易になります。

以下の図は、再現最小化のアプローチを視覚的に示しています。

mermaidflowchart TD
  complex["複雑な要件"] --> divide["小さな単位に分割"]
  divide --> step1["ステップ1<br/>ユーザー取得"]
  divide --> step2["ステップ2<br/>注文履歴取得"]
  divide --> step3["ステップ3<br/>推奨商品生成"]

  step1 --> verify1["検証1"]
  step2 --> verify2["検証2"]
  step3 --> verify3["検証3"]

  verify1 --> integrate["統合"]
  verify2 --> integrate
  verify3 --> integrate

  integrate --> final["完成コード"]

  style step1 fill:#ccffcc
  style step2 fill:#ccffcc
  style step3 fill:#ccffcc
  style final fill:#ccccff

解決策 2:拘束条件の明示

Codex に制約条件を明確に伝えることで、より正確なコードを生成できます。プロンプトに以下の情報を含めることが重要です。

拘束条件として含めるべき情報

#項目具体例
1使用する技術スタックTypeScript, Node.js 18, PostgreSQL
2エラーハンドリング方針404 エラー、500 エラーの返し方
3データ形式・型定義インターフェースや型の明示
4パフォーマンス要件クエリ回数の制限、キャッシュの使用
5セキュリティ要件認証、バリデーション、エスケープ
6ビジネスルールドメイン固有の制約条件

実践例:制約条件を明示したプロンプト

まず、曖昧なプロンプトの例を見てみましょう。

typescript// 悪い例:曖昧なプロンプト
// "ユーザーを作成する関数を作って"

// Codex が生成したコード(制約が考慮されていない)
async function createUser(name: string, email: string) {
  const user = await db.insert('users', { name, email });
  return user;
}

このコードには以下の問題があります:

  • メールアドレスの重複チェックがない
  • バリデーションがない
  • エラーハンドリングが不足
  • 型定義が曖昧

次に、制約条件を明示した良い例を見てみます。

typescript// 良い例:詳細な制約条件を含むプロンプト
/**
 * ユーザー作成関数の要件:
 * - TypeScript で型安全に実装
 * - メールアドレスは一意である必要がある(重複時は Error: 'DUPLICATE_EMAIL' を throw)
 * - メールアドレスは正規表現で検証(無効な場合は Error: 'INVALID_EMAIL' を throw)
 * - 名前は 1-50 文字(範囲外は Error: 'INVALID_NAME_LENGTH' を throw)
 * - 作成日時は自動設定
 * - トランザクション内で実行
 */

上記のプロンプトに基づいて、以下のような型定義を追加します。

typescript// 型定義を明示
interface CreateUserInput {
  name: string;
  email: string;
}

interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

// カスタムエラー型
class UserCreationError extends Error {
  constructor(
    public code:
      | 'DUPLICATE_EMAIL'
      | 'INVALID_EMAIL'
      | 'INVALID_NAME_LENGTH',
    message: string
  ) {
    super(message);
    this.name = 'UserCreationError';
  }
}

次に、バリデーション処理を実装します。

typescript// バリデーション関数
function validateEmail(email: string): void {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  if (!emailRegex.test(email)) {
    throw new UserCreationError(
      'INVALID_EMAIL',
      `Invalid email format: ${email}`
    );
  }
}

function validateName(name: string): void {
  if (name.length < 1 || name.length > 50) {
    throw new UserCreationError(
      'INVALID_NAME_LENGTH',
      `Name length must be 1-50 characters, got: ${name.length}`
    );
  }
}

バリデーション関数を独立させることで、テストが容易になり、再利用も可能になります。

typescript// 重複チェック関数
async function checkEmailDuplicate(
  email: string
): Promise<void> {
  const result = await db.query(
    'SELECT id FROM users WHERE email = $1',
    [email]
  );

  if (result.rows.length > 0) {
    throw new UserCreationError(
      'DUPLICATE_EMAIL',
      `Email already exists: ${email}`
    );
  }
}

重複チェックもデータベースアクセスを伴うため、別関数として切り出します。

typescript// 最終的なユーザー作成関数
async function createUser(
  input: CreateUserInput
): Promise<User> {
  const { name, email } = input;

  // バリデーション実行
  validateName(name);
  validateEmail(email);

  // トランザクション開始
  const client = await db.getClient();

  try {
    await client.query('BEGIN');

    // 重複チェック
    await checkEmailDuplicate(email);

    // ユーザー作成
    const result = await client.query(
      'INSERT INTO users (name, email, created_at) VALUES ($1, $2, NOW()) RETURNING *',
      [name, email]
    );

    await client.query('COMMIT');

    return result.rows[0];
  } catch (error) {
    // エラー時はロールバック
    await client.query('ROLLBACK');
    throw error;
  } finally {
    // クライアントを必ず解放
    client.release();
  }
}

制約条件を明示することで、エラーハンドリング、バリデーション、トランザクション管理が適切に実装されたコードが生成されます。

解決策 3:テスト駆動開発(TDD)の適用

テスト駆動開発(Test-Driven Development, TDD)を Codex と組み合わせることで、コードの品質を大幅に向上できます。先にテストを書き、そのテストをパスするコードを Codex に生成させるアプローチです。

TDD のサイクル

mermaidflowchart LR
  test_write["テスト作成"] --> test_fail["テスト失敗<br/>(Red)"]
  test_fail --> code_gen["Codexでコード生成"]
  code_gen --> test_pass["テスト成功<br/>(Green)"]
  test_pass --> refactor["リファクタリング"]
  refactor --> test_write

  style test_fail fill:#ffcccc
  style test_pass fill:#ccffcc
  style refactor fill:#ccccff

図で理解できる要点

  • テストを先に作成し、最初は失敗(Red)させます
  • Codex にコードを生成させ、テストが成功(Green)するまで調整します
  • 成功後にリファクタリングを行い、品質を向上させます

実践例:TDD で Codex を活用

まず、テストケースを作成します。テストを先に書くことで、期待する動作を明確にできます。

typescript// ステップ1: テストケースを作成(Jest使用)

import { describe, it, expect } from '@jest/globals';
import { calculateDiscount } from './discount';

describe('calculateDiscount', () => {
  it('通常会員は10%割引を適用', () => {
    const result = calculateDiscount(1000, 'regular');
    expect(result).toBe(900);
  });

  it('プレミアム会員は20%割引を適用', () => {
    const result = calculateDiscount(1000, 'premium');
    expect(result).toBe(800);
  });
});

基本的なテストケースから始めることで、Codex に期待する動作を明確に伝えられます。

typescript// さらにエッジケースのテストを追加

describe('calculateDiscount - エッジケース', () => {
  it('0円の場合は0円を返す', () => {
    const result = calculateDiscount(0, 'regular');
    expect(result).toBe(0);
  });

  it('負の金額の場合はエラーをthrow', () => {
    expect(() => {
      calculateDiscount(-100, 'regular');
    }).toThrow('Price must be non-negative');
  });

  it('不正な会員タイプの場合はエラーをthrow', () => {
    expect(() => {
      calculateDiscount(1000, 'invalid' as any);
    }).toThrow('Invalid member type');
  });
});

エッジケースを含めることで、Codex が生成するコードの堅牢性が向上します。

typescript// ステップ2: テストに基づいてプロンプトを作成

/**
 * calculateDiscount 関数を実装してください。
 *
 * 要件:
 * - 第1引数: price (number) - 商品価格
 * - 第2引数: memberType ('regular' | 'premium') - 会員タイプ
 * - 戻り値: 割引後の価格 (number)
 *
 * ビジネスルール:
 * - regular: 10% 割引
 * - premium: 20% 割引
 * - 価格が0の場合は0を返す
 * - 負の価格の場合は "Price must be non-negative" エラーをthrow
 * - 不正な会員タイプの場合は "Invalid member type" エラーをthrow
 */

このプロンプトをもとに、Codex に実装を生成させます。

typescript// ステップ3: Codex が生成したコード

type MemberType = 'regular' | 'premium';

function calculateDiscount(
  price: number,
  memberType: MemberType
): number {
  // 負の価格チェック
  if (price < 0) {
    throw new Error('Price must be non-negative');
  }

  // 0円の場合はそのまま返す
  if (price === 0) {
    return 0;
  }

  // 会員タイプに応じた割引率を適用
  switch (memberType) {
    case 'regular':
      return price * 0.9; // 10% 割引
    case 'premium':
      return price * 0.8; // 20% 割引
    default:
      throw new Error('Invalid member type');
  }
}

export { calculateDiscount };

テストケースに基づいてコードが生成されるため、要件を満たす可能性が高くなります。

typescript// ステップ4: テストを実行して検証

// コマンド実行: yarn test discount.test.ts
//
// 実行結果:
// PASS  ./discount.test.ts
//   calculateDiscount
//     ✓ 通常会員は10%割引を適用 (2 ms)
//     ✓ プレミアム会員は20%割引を適用 (1 ms)
//   calculateDiscount - エッジケース
//     ✓ 0円の場合は0円を返す
//     ✓ 負の金額の場合はエラーをthrow (1 ms)
//     ✓ 不正な会員タイプの場合はエラーをthrow
//
// Test Suites: 1 passed, 1 total
// Tests:       5 passed, 5 total

すべてのテストが成功すれば、Codex が生成したコードが要件を満たしていることが確認できます。

TDD の利点

TDD を Codex と組み合わせることで、以下のメリットが得られます。

#利点説明
1要件の明確化テストが仕様書の役割を果たす
2バグの早期発見コード生成直後に問題を検出できる
3リファクタリングの安全性テストがあるため、安心して改善できる
4ドキュメント化テストコードが使用例として機能する
5回帰テスト将来の変更で既存機能が壊れないことを保証

3 つの解決策を組み合わせる

これら 3 つのアプローチを組み合わせることで、Codex の精度を最大化できます。以下の図は、実践的なワークフローを示しています。

mermaidflowchart TD
  start["要件定義"] --> constraints["拘束条件の整理"]
  constraints --> divide["再現最小化<br/>(機能分割)"]
  divide --> test["テスト作成<br/>(TDD)"]
  test --> prompt["詳細プロンプト作成"]
  prompt --> codex["Codex でコード生成"]
  codex --> verify["テスト実行"]
  verify --> pass{成功?}
  pass -->|Yes| next["次の機能へ"]
  pass -->|No| refine["プロンプト改善"]
  refine --> codex

  style pass fill:#ffffcc
  style next fill:#ccffcc

具体例

ここでは、実際の開発シナリオを想定して、3 つの解決策を統合的に適用する具体例をご紹介します。

シナリオ:ブログ記事の公開システム

ブログ記事を公開する機能を実装するとします。要件は以下の通りです。

  • 記事のタイトル、本文、著者 ID を受け取る
  • 記事をデータベースに保存する
  • 公開ステータスは「下書き」または「公開済み」
  • タイトルは 1-100 文字必須
  • 本文は 1 文字以上必須
  • 著者が存在しない場合はエラー

ステップ 1:拘束条件を整理

まず、要件を明確な拘束条件として整理します。

typescript// 拘束条件の定義

/**
 * ブログ記事公開機能の制約:
 *
 * 技術スタック:
 * - TypeScript 5.0+
 * - PostgreSQL 15
 * - Node.js 18+
 *
 * データ制約:
 * - タイトル: 1-100文字(必須)
 * - 本文: 1文字以上(必須)
 * - 著者ID: UUID形式(存在チェック必須)
 * - ステータス: 'draft' | 'published'
 *
 * エラーハンドリング:
 * - INVALID_TITLE_LENGTH: タイトル長が範囲外
 * - INVALID_CONTENT: 本文が空
 * - AUTHOR_NOT_FOUND: 著者が存在しない
 * - DATABASE_ERROR: DB操作エラー
 */

制約条件を文書化することで、Codex に正確な指示を出せます。

typescript// 型定義

interface CreateArticleInput {
  title: string;
  content: string;
  authorId: string;
  status: 'draft' | 'published';
}

interface Article {
  id: string;
  title: string;
  content: string;
  authorId: string;
  status: 'draft' | 'published';
  createdAt: Date;
  updatedAt: Date;
}

class ArticleError extends Error {
  constructor(
    public code:
      | 'INVALID_TITLE_LENGTH'
      | 'INVALID_CONTENT'
      | 'AUTHOR_NOT_FOUND'
      | 'DATABASE_ERROR',
    message: string
  ) {
    super(message);
    this.name = 'ArticleError';
  }
}

ステップ 2:再現最小化で機能を分割

複雑な処理を小さな関数に分割します。

typescript// 分割1: タイトルバリデーション

function validateTitle(title: string): void {
  if (title.length < 1 || title.length > 100) {
    throw new ArticleError(
      'INVALID_TITLE_LENGTH',
      `Title must be 1-100 characters, got ${title.length}`
    );
  }
}
typescript// 分割2: 本文バリデーション

function validateContent(content: string): void {
  if (content.length < 1) {
    throw new ArticleError(
      'INVALID_CONTENT',
      'Content must not be empty'
    );
  }
}
typescript// 分割3: 著者存在チェック

async function checkAuthorExists(
  authorId: string
): Promise<void> {
  const result = await db.query(
    'SELECT id FROM authors WHERE id = $1',
    [authorId]
  );

  if (result.rows.length === 0) {
    throw new ArticleError(
      'AUTHOR_NOT_FOUND',
      `Author not found: ${authorId}`
    );
  }
}

各バリデーション処理を独立した関数として実装することで、テストと再利用が容易になります。

ステップ 3:テスト駆動開発で品質保証

先にテストケースを作成します。

typescript// テストファイル: article.test.ts

import {
  describe,
  it,
  expect,
  beforeEach,
} from '@jest/globals';
import { createArticle } from './article';

describe('createArticle', () => {
  // 正常系テスト
  it('有効な入力で記事を作成できる', async () => {
    const input = {
      title: 'テスト記事',
      content: 'これはテスト記事の本文です',
      authorId: 'valid-author-id',
      status: 'draft' as const,
    };

    const article = await createArticle(input);

    expect(article.title).toBe('テスト記事');
    expect(article.status).toBe('draft');
    expect(article.id).toBeDefined();
  });
});
typescript// エラーケースのテスト

describe('createArticle - バリデーション', () => {
  it('タイトルが長すぎる場合はエラー', async () => {
    const input = {
      title: 'a'.repeat(101), // 101文字
      content: '本文',
      authorId: 'valid-author-id',
      status: 'draft' as const,
    };

    await expect(createArticle(input)).rejects.toThrow(
      'INVALID_TITLE_LENGTH'
    );
  });

  it('本文が空の場合はエラー', async () => {
    const input = {
      title: 'タイトル',
      content: '',
      authorId: 'valid-author-id',
      status: 'draft' as const,
    };

    await expect(createArticle(input)).rejects.toThrow(
      'INVALID_CONTENT'
    );
  });
});
typescript// 著者チェックのテスト

describe('createArticle - 著者チェック', () => {
  it('存在しない著者IDの場合はエラー', async () => {
    const input = {
      title: 'タイトル',
      content: '本文',
      authorId: 'non-existent-author',
      status: 'draft' as const,
    };

    await expect(createArticle(input)).rejects.toThrow(
      'AUTHOR_NOT_FOUND'
    );
  });
});

テストを先に書くことで、実装すべき機能とエラーケースが明確になります。

ステップ 4:Codex でコード生成

テストと制約条件を踏まえて、Codex に実装を生成させます。

typescript// プロンプト:
// "上記のテストケースをすべてパスする createArticle 関数を実装してください。
//  validateTitle, validateContent, checkAuthorExists 関数を使用し、
//  トランザクション内で記事を作成してください。"

// Codex が生成したコード

async function createArticle(
  input: CreateArticleInput
): Promise<Article> {
  const { title, content, authorId, status } = input;

  // バリデーション実行
  validateTitle(title);
  validateContent(content);
  await checkAuthorExists(authorId);

  // トランザクション開始
  const client = await db.getClient();

  try {
    await client.query('BEGIN');

    const result = await client.query(
      `INSERT INTO articles
       (title, content, author_id, status, created_at, updated_at)
       VALUES ($1, $2, $3, $4, NOW(), NOW())
       RETURNING *`,
      [title, content, authorId, status]
    );

    await client.query('COMMIT');

    return result.rows[0];
  } catch (error) {
    await client.query('ROLLBACK');

    throw new ArticleError(
      'DATABASE_ERROR',
      `Failed to create article: ${error.message}`
    );
  } finally {
    client.release();
  }
}

export { createArticle };

ステップ 5:テスト実行と検証

生成されたコードがテストをパスするか確認します。

typescript// テスト実行
// コマンド: yarn test article.test.ts

// 実行結果:
// PASS  ./article.test.ts
//   createArticle
//     ✓ 有効な入力で記事を作成できる (15 ms)
//   createArticle - バリデーション
//     ✓ タイトルが長すぎる場合はエラー (5 ms)
//     ✓ 本文が空の場合はエラー (4 ms)
//   createArticle - 著者チェック
//     ✓ 存在しない著者IDの場合はエラー (8 ms)
//
// Test Suites: 1 passed, 1 total
// Tests:       4 passed, 4 total
// Snapshots:   0 total
// Time:        1.234 s

すべてのテストが成功すれば、実装完了です。

成功のポイント

この具体例で適用したポイントを整理します。

#ポイント効果
1拘束条件の明示エラーハンドリングとバリデーションが適切に実装された
2機能の最小化各関数が独立してテスト可能になった
3テスト駆動コード生成前に期待動作を定義し、品質を保証した
4段階的な統合検証済みの関数を組み合わせることで、バグを最小化した

以下の図は、統合的なワークフローの全体像を示しています。

mermaidsequenceDiagram
  participant Dev as 開発者
  participant Test as テスト
  participant Codex as Codex
  participant Code as コード

  Dev->>Dev: 拘束条件整理
  Dev->>Dev: 機能分割
  Dev->>Test: テストケース作成
  Test->>Test: 実行(Red)
  Dev->>Codex: 詳細プロンプト送信
  Codex->>Code: コード生成
  Code->>Test: テスト実行
  Test-->>Dev: 結果(Green/Red)

  alt テスト失敗
    Dev->>Codex: プロンプト改善
    Codex->>Code: 再生成
    Code->>Test: 再テスト
  end

  Test-->>Dev: すべて成功
  Dev->>Dev: 次の機能へ

図で理解できる要点

  • 開発者が制約を整理し、テストを先に作成します
  • Codex にコードを生成させ、テストで検証します
  • 失敗した場合はプロンプトを改善して再生成します
  • すべてのテストが成功するまで繰り返します

まとめ

Codex が誤ったコードを出力する問題に対して、本記事では 3 つの実践的なアプローチをご紹介しました。

再現最小化により、複雑な処理を小さな単位に分割し、問題箇所の特定を容易にできます。拘束条件の明示により、Codex に正確な要件を伝え、期待通りのコードを生成させることができます。そして テスト駆動開発 により、コード生成前に期待動作を定義し、品質を保証できるのです。

これら 3 つの手法を組み合わせることで、Codex の生産性を最大限に引き出しながら、コード品質を維持することが可能になります。

最初は手間がかかるように感じるかもしれませんが、長期的には開発効率とコード品質の両方が向上し、結果として開発時間の短縮につながるでしょう。ぜひ、日々の開発に取り入れてみてください。

AI コーディングアシスタントは、適切に活用すれば強力なパートナーになります。本記事でご紹介した手法を実践することで、Codex との協働がより快適で生産的なものになることを願っています。

関連リンク