🤖

ChatGPT (o1-preview) にテストを渡してコードを実装させるとどうなるか試した

2024/09/15に公開
2

はじめに

前にも別のモデルでやってる

https://zenn.dev/sterashima78/articles/7876bd00aa2115

https://zenn.dev/sterashima78/articles/8bca47a054fabd

ただ o1-preview は、やり取りを重ねるよりも一発で終わらせるほうがいいらしいので、最終的なテスト全体を渡すようにした。

情報の提示方法が異なると当然結果も変わるので、 gpt-4o でも同様なことを試した。

材料

プロンプトは以下。
基本的に最初にやったときと同じ。ペアプロではないのでその部分の調整をしている

- 私がテストコードを提示するのでそのテストケースをパスする最小限の実装をしてください
- Vue.js のバージョン 3 と Typescript で実装を行ってください
- コードのみを示してくださいコードの解説などは必要ありません
- スタイリングは必要ありません
- テストケースに失敗したらその内容をチャットで送信するので最小限のコードの修正をしてください
- テストのコードには vitest を利用しています jest と互換がある API なので、vitest のことを知らなければ jest を使っていると思ってください
- テストの記述には testing-library/vue を利用します

import { render, waitFor } from '@testing-library/vue';
import { userEvent } from '@testing-library/user-event';
import App from './App.vue';

describe('Test for App', () => {
  let canvas: ReturnType<typeof render>;
  beforeEach(() => {
    canvas = render(App);
  });

  test('test', () => {
    expect(canvas).not.toBeNull();
  });

  test('入力したテキストに応じてアイテムができる', async () => {
    const textbox = await canvas.findByRole<HTMLInputElement>('textbox', {
      name: 'タイトル',
    });
    textbox.focus();
    userEvent.keyboard('item1');
    await waitFor(() => {
      expect(textbox.value).toBe('item1');
    });
    userEvent.keyboard('{enter}');
    await waitFor(() => {
      expect(textbox.value).toBe('');
    });
    await canvas.findByRole('listitem', {
      name: 'item1',
    });
    await canvas.findByText('1 item left');
  });
  test('アイテムを削除できる', async () => {
    const textbox = await canvas.findByRole<HTMLInputElement>('textbox', {
      name: 'タイトル',
    });
    textbox.focus();
    userEvent.keyboard('item1');
    await waitFor(() => {
      expect(textbox.value).toBe('item1');
    });

    userEvent.keyboard('{enter}');
    await waitFor(() => {
      expect(textbox.value).toBe('');
    });
    const item = await canvas.findByRole('listitem', {
      name: 'item1',
    });
    const btn = await canvas.findByRole('button', {
      name: 'item1 を 削除する',
    });
    userEvent.click(btn);
    await waitFor(() => {
      expect(canvas.queryByRole('listitem', { name: 'item1' })).toBeNull();
    });
  });

  test('アイテムを完了できる', async () => {
    const textbox = await canvas.findByRole<HTMLInputElement>('textbox', {
      name: 'タイトル',
    });
    textbox.focus();
    userEvent.keyboard('item1');
    await waitFor(() => {
      expect(textbox.value).toBe('item1');
    });
    userEvent.keyboard('{enter}');
    await waitFor(() => {
      expect(textbox.value).toBe('');
    });
    await canvas.findByRole('listitem', {
      name: 'item1',
    });
    await canvas.findByText('1 item left');

    const checkbox = await canvas.findByRole('checkbox', {
      name: 'item1 を完了する',
    });
    userEvent.click(checkbox);
    await canvas.findByText('0 items left');
  });

  test('表示項目を切り替えられる', async () => {
    const textbox = await canvas.findByRole<HTMLInputElement>('textbox', {
      name: 'タイトル',
    });
    textbox.focus();
    userEvent.keyboard('item1');
    await waitFor(() => {
      expect(textbox.value).toBe('item1');
    });
    userEvent.keyboard('{enter}');
    await waitFor(() => {
      expect(textbox.value).toBe('');
    });
    userEvent.keyboard('item2');
    await waitFor(() => {
      expect(textbox.value).toBe('item2');
    });
    userEvent.keyboard('{enter}');
    await waitFor(() => {
      expect(textbox.value).toBe('');
    });
    expect(await canvas.findAllByRole('listitem')).toHaveLength(2);
    await canvas.findByText('2 items left');

    const checkbox = await canvas.findByRole('checkbox', {
      name: 'item1 を完了する',
    });
    userEvent.click(checkbox);
    await canvas.findByText('1 item left');

    // Active
    userEvent.click(await canvas.findByRole('button', { name: 'Active' }));
    await waitFor(async () => {
      expect(await canvas.findAllByRole('listitem')).toHaveLength(1);
      await canvas.findByRole('listitem', {
        name: 'item2',
      });
    });
    // All
    userEvent.click(await canvas.findByRole('button', { name: 'All' }));
    await waitFor(async () => {
      expect(await canvas.findAllByRole('listitem')).toHaveLength(2);
    });
    // Completed
    userEvent.click(await canvas.findByRole('button', { name: 'Completed' }));
    await waitFor(async () => {
      expect(await canvas.findAllByRole('listitem')).toHaveLength(1);
      await canvas.findByRole('listitem', {
        name: 'item1',
      });
    });
  });
  test('完了済み項目を削除できる', async () => {
    const textbox = await canvas.findByRole<HTMLInputElement>('textbox', {
      name: 'タイトル',
    });
    textbox.focus();
    // add item1
    userEvent.keyboard('item1');
    await waitFor(() => {
      expect(textbox.value).toBe('item1');
    });
    userEvent.keyboard('{enter}');
    await waitFor(() => {
      expect(textbox.value).toBe('');
    });
    // add item2
    userEvent.keyboard('item2');
    await waitFor(() => {
      expect(textbox.value).toBe('item2');
    });
    userEvent.keyboard('{enter}');
    await waitFor(() => {
      expect(textbox.value).toBe('');
    });
    // add item3
    userEvent.keyboard('item3');
    await waitFor(() => {
      expect(textbox.value).toBe('item3');
    });
    userEvent.keyboard('{enter}');
    await waitFor(() => {
      expect(textbox.value).toBe('');
    });
    expect(await canvas.findAllByRole('listitem')).toHaveLength(3);
    await canvas.findByText('3 items left');

    userEvent.click(
      await canvas.findByRole('checkbox', {
        name: 'item1 を完了する',
      })
    );
    userEvent.click(
      await canvas.findByRole('checkbox', {
        name: 'item3 を完了する',
      })
    );
    await canvas.findByText('1 item left');
    // Completed
    userEvent.click(await canvas.findByRole('button', { name: 'Completed' }));
    await waitFor(async () => {
      expect(await canvas.findAllByRole('listitem')).toHaveLength(2);
    });
    // Active
    userEvent.click(await canvas.findByRole('button', { name: 'Active' }));
    await waitFor(async () => {
      expect(await canvas.findAllByRole('listitem')).toHaveLength(1);
    });
    // All
    userEvent.click(await canvas.findByRole('button', { name: 'All' }));
    await waitFor(async () => {
      expect(await canvas.findAllByRole('listitem')).toHaveLength(3);
    });
    // Clear Complete
    userEvent.click(
      await canvas.findByRole('button', { name: 'Clear Completed' })
    );
    await waitFor(async () => {
      expect(await canvas.findAllByRole('listitem')).toHaveLength(1);
      await canvas.findByRole('listitem', { name: 'item2' });
    });
  });

  test('項目のテキストを修正できる', async () => {
    const textbox = await canvas.findByRole<HTMLInputElement>('textbox', {
      name: 'タイトル',
    });
    textbox.focus();
    // add item1
    userEvent.keyboard('item1');
    await waitFor(() => {
      expect(textbox.value).toBe('item1');
    });
    userEvent.keyboard('{enter}');
    await waitFor(() => {
      expect(textbox.value).toBe('');
    });
    const item = await canvas.findByText('item1');
    userEvent.dblClick(item);
    await waitFor(async () => {
      expect(
        await canvas.findByRole('textbox', {
          name: 'item1 を編集',
        })
      ).toBe(document.activeElement);
    });
    await userEvent.keyboard(
      '{backspace}{backspace}{backspace}{backspace}{backspace}new item{enter}'
    );
    await canvas.findByRole('listitem', {
      name: 'new item',
    });
  });
});

結果

GPT-4o

以下のようなコードを返した。

<template>
  <div>
    <input
      v-model="title"
      placeholder="タイトル"
      @keydown.enter="addItem"
      aria-label="タイトル"
    />
    <ul>
      <li v-for="item in filteredItems" :key="item.id">
        <input
          type="checkbox"
          :checked="item.completed"
          @change="toggleComplete(item.id)"
          :aria-label="`${item.text} を完了する`"
        />
        <span v-if="!item.editing">{{ item.text }}</span>
        <input
          v-else
          v-model="item.text"
          @keydown.enter="saveItem(item.id)"
          aria-label="item を編集"
        />
        <button @click="deleteItem(item.id)" :aria-label="`${item.text} を 削除する`">
          削除
        </button>
      </li>
    </ul>
    <div>{{ remainingItems }} item left</div>
    <button @click="filter = 'all'">All</button>
    <button @click="filter = 'active'">Active</button>
    <button @click="filter = 'completed'">Completed</button>
    <button @click="clearCompleted">Clear Completed</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed } from 'vue';

interface Item {
  id: number;
  text: string;
  completed: boolean;
  editing: boolean;
}

export default defineComponent({
  setup() {
    const title = ref('');
    const items = ref<Item[]>([]);
    const filter = ref('all');

    const addItem = () => {
      if (title.value.trim()) {
        items.value.push({
          id: Date.now(),
          text: title.value,
          completed: false,
          editing: false,
        });
        title.value = '';
      }
    };

    const deleteItem = (id: number) => {
      items.value = items.value.filter((item) => item.id !== id);
    };

    const toggleComplete = (id: number) => {
      const item = items.value.find((item) => item.id === id);
      if (item) {
        item.completed = !item.completed;
      }
    };

    const saveItem = (id: number) => {
      const item = items.value.find((item) => item.id === id);
      if (item) {
        item.editing = false;
      }
    };

    const filteredItems = computed(() => {
      if (filter.value === 'active') {
        return items.value.filter((item) => !item.completed);
      } else if (filter.value === 'completed') {
        return items.value.filter((item) => item.completed);
      } else {
        return items.value;
      }
    });

    const remainingItems = computed(() => {
      return items.value.filter((item) => !item.completed).length;
    });

    const clearCompleted = () => {
      items.value = items.value.filter((item) => !item.completed);
    };

    return {
      title,
      items,
      filter,
      addItem,
      deleteItem,
      toggleComplete,
      saveItem,
      filteredItems,
      remainingItems,
      clearCompleted,
    };
  },
});
</script>

テスト結果は以下でパスしなかった

❯ src/index.test.ts (7) 6863ms
   ❯ Test for App (7) 6862ms
     ✓ test
     × 入力したテキストに応じてアイテムができる 1164ms
     × アイテムを削除できる 1083ms
     × アイテムを完了できる 1081ms
     × 表示項目を切り替えられる 1170ms
     × 完了済み項目を削除できる 1219ms
     × 項目のテキストを修正できる 1081ms

o1-preview

以下のようなコードを返した

<template>
  <input
    v-model="newItem"
    @keyup.enter="addItem"
    role="textbox"
    :aria-label="'タイトル'"
  />

  <ul>
    <li
      v-for="item in filteredItems"
      :key="item.id"
      role="listitem"
      :aria-label="item.text"
    >
      <input
        type="checkbox"
        v-model="item.completed"
        :aria-label="`${item.text} を完了する`"
      />
      <span v-if="editingId !== item.id" @dblclick="startEditing(item)">
        {{ item.text }}
      </span>
      <input
        v-else
        v-model="item.text"
        @keyup.enter="stopEditing"
        @blur="stopEditing"
        role="textbox"
        :aria-label="`${item.text} を編集`"
        autofocus
      />
      <button @click="deleteItem(item)" :aria-label="`${item.text} を 削除する`">
        Delete
      </button>
    </li>
  </ul>

  <div>{{ itemsLeftText }}</div>

  <button @click="filter='all'" :aria-label="'All'">All</button>
  <button @click="filter='active'" :aria-label="'Active'">Active</button>
  <button @click="filter='completed'" :aria-label="'Completed'">Completed</button>

  <button @click="clearCompleted" :aria-label="'Clear Completed'">
    Clear Completed
  </button>
</template>

<script lang="ts">
import { defineComponent, reactive, computed, ref, nextTick } from 'vue';

export default defineComponent({
  setup() {
    const newItem = ref('');
    const items = reactive<{ id: number; text: string; completed: boolean }[]>(
      []
    );
    const filter = ref('all');
    const editingId = ref<number | null>(null);

    const addItem = () => {
      if (newItem.value.trim()) {
        items.push({
          id: Date.now(),
          text: newItem.value.trim(),
          completed: false,
        });
        newItem.value = '';
      }
    };

    const deleteItem = (item: any) => {
      const index = items.indexOf(item);
      if (index > -1) {
        items.splice(index, 1);
      }
    };

    const clearCompleted = () => {
      for (let i = items.length - 1; i >= 0; i--) {
        if (items[i].completed) {
          items.splice(i, 1);
        }
      }
    };

    const itemsLeft = computed(() => items.filter((item) => !item.completed).length);

    const itemsLeftText = computed(() => {
      const count = itemsLeft.value;
      return `${count} item${count === 1 ? '' : 's'} left`;
    });

    const filteredItems = computed(() => {
      if (filter.value === 'active') {
        return items.filter((item) => !item.completed);
      } else if (filter.value === 'completed') {
        return items.filter((item) => item.completed);
      } else {
        return items;
      }
    });

    const startEditing = (item: any) => {
      editingId.value = item.id;
      nextTick(() => {
        const input = document.querySelector(
          `input[aria-label="${item.text} を編集"]`
        ) as HTMLInputElement;
        if (input) {
          input.focus();
        }
      });
    };

    const stopEditing = () => {
      editingId.value = null;
    };

    return {
      newItem,
      items,
      addItem,
      deleteItem,
      itemsLeftText,
      filteredItems,
      filter,
      clearCompleted,
      editingId,
      startEditing,
      stopEditing,
    };
  },
});
</script>

テストはすべてパスした (!?)

 ✓ src/index.test.ts (7) 1245ms
   ✓ Test for App (7) 1244ms
     ✓ test
     ✓ 入力したテキストに応じてアイテムができる
     ✓ アイテムを削除できる
     ✓ アイテムを完了できる
     ✓ 表示項目を切り替えられる
     ✓ 完了済み項目を削除できる 308ms
     ✓ 項目のテキストを修正できる

また、o1-pewview は「考える」ということをするため、
思考時間と思考内容というものも表示される。
思考時間は 26秒で、思考内容は以下だった。

要求の確定

ユーザーは、Vue.js 3とTypeScriptを使っているコンポーネントの実装を求めていて、指定されたテストケースを通過する最小限のコードを提供するようお願いしています。

テストの理解

canvasがnullでないことを確認しているど、基本的なチェックをしている。App.vueが有効なコンポーネントである限り、これが成立している。

テキストの入力としてアイテムを追加し、削除するテストを進行中

'タイトル'と名前が入力されたテキストボックスを見つけて、'item1'を追加し、リストアイテムに表示されて、削除ボタンをクリックしてDOMから削除されるか確認している。

アイテムの取り扱い

アイテムに削除ボタンがあると便利だね。「item1を削除する」という名前で。Checkboxを使って完了を示し、残りのアイテム数が更新されるんだ。

アイテムの完了を示す

Checkboxを使い、アイテムを完了できると「0 items left」というテキストが表示される。これまた、アイテム管理がスムーズになるね。

表示項目の切り替え

「item1」と「item2」を追加し、「item1」を完了にする。これにより、表示項目を切り替えることができ、効率的にアイテムを管理する方法を提案している。

項目の操作

ボタンを操作してビューを切り替え、未完了項目を表示し、完了済み項目を削除し、項目のテキストを編集する。

To perform various actions in the todo list app, such as adding, displaying, completing, deleting, editing, filtering, and clearing items, a detailed understanding of Vue.js 3 with TypeScript is required.

基本構造を定義する

まずは、App.vueコンポーネントの基本的な構造を定義し始めている。次に、コンポーネントの内部に必要な要素やロジックを具体的に追加していく予定だ。

ユーザビリティの向上

リスト項目をダブルクリックすると、インライン編集できるようにする計画。アリアラベルを編集用に 'item1 を編集' に設定して、視覚障害者への配慮も忘れない技術的な対策をとっているところ。

新しい編集者の追跡

編集中の項目を追跡するために、データに editingId を追加し、テンプレートとスクリプトを更新している。

項目の管理をサポート

リストの項目を追加、削除、編集し、完了済みの項目を一括で削除する機能を提供している。フィルタリングやアラビラベルの設定も含まれている。

おわりに

賢い。

自分の要求を正確に記述することの重要性がますます高まっていることがわかる。
要求の記述方法としてテストコードはわかりやすいと言えるのでやはり有効な手段だろう。

反復的なタスクになりがちな要求やテストに関係する仕様の整理は GPT-4o 使って、それを o1-preview に突っ込んでテストコード作って、更にそれを o1-preview に突っ込んで実装を作るというのが見えてきているという感じがする。

当然それなりの規模のシステムやソフトウェアを一発で作るのは難しいだろうから、抽象的なレベルで分割してサブシステム事に実装させるなどの工夫も必要だろうが、それすら生成 AI の補助を受けられるような気がする。

ただこれは開発者が不要になるということではない。一人の開発者が担える領域と量が非常に大きくなるということだ。

Discussion

おとかおとか

やっぱりo1えぐいっすねw

4oで要件や仕様詰めて、疾走部分のみをo1に渡すのが賢そう。プロンプト自体も4oで作っちゃったりするとさらに楽できそうですね

sterashima78sterashima78

それはそうなのですが、成果物の妥当性は人間が判断するしかないのでどうやってそこを担保するかがポイントになると思っています。

生成 AI に作らせたものに対して意図通りになっているかを確認するより、自分で作るほうが誤って意図から外れることが少ないと私は思っています。

なので、

  • 人間だけで作る
  • AI の補助を受けつつ人間が作る
  • 全て AI が作って人間は確認する

というのをどのステップに対して適用するかをよく考えることが実用上は大事だと思っています。