AI駆動開発を調べていたらCursorと出会った話
はじめに
FizzBuzz問題を解くコードをCursorで生成してみました。
ただ、CursorはAIでコードを生成させるだけではなく、テスト駆動開発を行ってみたいと思います。
参考資料の本を読めばCursorについては理解できるが、活用方法をハンズオン形式で記事にした方が馴染みやすいと思ったので今回の記事を作成しました。
Cursorとは
The AI Code Editor
CursorはAIによるコード生成をサポートするコードエディタです。
CursorはVSCodeの拡張機能としてインストールできます。
FizzBuzzとは
元は単純なパーティゲームでFizzBuzzはプログラミングの入門書でよく出てくる問題です。
– 2人以上のプレイヤーが1から順番に数字を発言していく
– 3で割り切れるときは「Fizz」を発言
– 5で割り切れるときは「Buzz」を発言
– 両方で割り切れるときは「FizzBuzz」を発言
– 間違えた人から脱落
プログラミングなので1人でやり、脱落もしませんのでプログラミングの仕様に落とし込むと下記のようになります
- 3で割り切れるときは「Fizz」を出力
- 5で割り切れるときは「Buzz」を出力
- 両方で割り切れるときは「FizzBuzz」を出力
- それ以外の場合は数字を出力
参考資料
ますみ / 生成AIエンジニアの著書
サクッと始めるAIコードエディタ【Cursor / VS Code / ChatGPT】
無料で読めるのでぜひ読んでみてください
上記本を参考に今回のハンズオン形式の記事を作成しました
インストール
CursorはVSCodeの拡張機能として活用できるので、VSCodeをインストールしている場合は拡張機能をそのまま活用することができます。
開発をしてみる
- Vue.js
- TypeScript
- Vuetify
- Vite
- Vitest
上記環境でプロジェクトを作成してみます。
npx create-vue@latest
✔ Project name: … vue-fizz-buzz-cursor
✔ Add TypeScript? … No / Yes Yes
✔ Add JSX Support? … No / Yes Yes
✔ Add Vue Router for Single Page Application development? … No / Yes No
✔ Add Pinia for state management? … No / Yes No
✔ Add Vitest for Unit Testing? … No / Yes Yes
✔ Add an End-to-End Testing Solution? › Playwright
✔ Add ESLint for code quality? › Yes
✔ Add Prettier for code formatting? … No / Yes Yes
Docsを追加する
特定のWebドメイン配下のページを把握させ学習させることができます。
この機能を使って下記技術を学習させます
この機能を使わなくてもある程度の精度は出せますが最大限のパフォーマンスを出すために設定します
- Vue.js
- TypeScript
- Vuetify ← この技術は何故かデフォルトで設定されている
- Vitest
command + K でDocsを開き@docsを入力してください
候補の中からDocsを選択してください
+Add new docsを選択して下記ドメインをドキュメントに追加してください
Vuetifyのインストール
npm install vuetify@next
main.tsにVuetifyをインストールします。
AInに質問することなく、Cursorの自動補完によって下記コードを生成しています。
import './assets/main.css'
import { createApp } from 'vue'
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css' // おそらくssrを使うとき必要
import { createVuetify } from 'vuetify'
import { mdi } from 'vuetify/iconsets/mdi'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import App from './App.vue'
const vuetify = createVuetify({
components,
directives,
icons: {
defaultSet: 'mdi',
sets: { mdi },
},
})
createApp(App).use(vuetify).mount('#app')
FizzBuzzのテストコードを作成する
FizzBuzzのテストコードを作成してみます。
テスト駆動するのに必要なファイルを作成します
- /src/components/__ tests __/FizzBuzz.spec.ts
- /src/components/FizzBuzz.vue
テストファイルのプロンプトの作成
Chatモードを開き下記プロンプトで生成します。
対象ファイルは/src/components/tests/FizzBuzz.spec.tsです
@Vitest FizzBuzzのテストコードを作成してください
Applyを押してファイルに反映させる事ができます
このコードはFizzBuzzコンポーネントを作成していないのでエラーが出ますが一旦このままにしておきます。
コンポーネントを作成時に修正するようにします
FizzBuzzコンポーネントを作成する
下記プロンプトでFizzBuzzコンポーネントを作成しました
対象ファイルを下記二つの複数を選択しました
- /src/components/FizzBuzz.vue
- /src/components/__ tests __/FizzBuzz.spec.ts
@Vue @TypeScript @Vuetify Compositionを利用してFizzBuzz.spec.tsをオールグリーンになるコンポーネントを作成してください
<template>
<div>
<p v-for="n in numbers" :key="n">{{ fizzBuzz(n) }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const numbers = ref<number[]>([1, 2, 3, 4, 5, 15, 30])
function fizzBuzz(n: number): string {
if (n % 15 === 0) return 'FizzBuzz'
if (n % 3 === 0) return 'Fizz'
if (n % 5 === 0) return 'Buzz'
return n.toString()
}
</script>
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import FizzBuzz from '../FizzBuzz.vue'
describe('FizzBuzz component', () => {
it('renders FizzBuzz correctly', () => {
const wrapper = mount(FizzBuzz)
const paragraphs = wrapper.findAll('p')
expect(paragraphs[0].text()).toBe('1')
expect(paragraphs[1].text()).toBe('2')
expect(paragraphs[2].text()).toBe('Fizz')
expect(paragraphs[3].text()).toBe('4')
expect(paragraphs[4].text()).toBe('Buzz')
expect(paragraphs[5].text()).toBe('FizzBuzz')
expect(paragraphs[6].text()).toBe('FizzBuzz')
})
})
忘れていた事がありました。
vueのコンポーネントをテストする際はコードの記述が変わるのでテストコード生成のプロンプトの際にも @Vue ドキュメントを指定しておくべきでした。
結果としてAIはテストコードも修正してくれましたので問題なく動きそうですが…。
画面に出力してみる
対象ファイルは下記になります
- /src/App.vue
- /src/components/FizzBuzz.vue
一旦App.vueは簡素にしておきます
<script setup lang="ts"></script>
<template>
<main></main>
</template>
プロンプトでコードを生成してみる
@Vue @TypeScript @Vuetify 画面にFizzBuzzコンポーネントを表示してください
<template>
<v-app>
<v-main>
<v-container>
<FizzBuzz />
</v-container>
</v-main>
</v-app>
</template>
<script setup lang="ts">
import FizzBuzz from './components/FizzBuzz.vue'
</script>
<style>
@import 'vuetify/styles';
</style>
# 実行確認
npm run dev
シンプルですが概ね期待通りの結果が出力されました
テスト結果の確認
画面上問題なく見えるかテストをパスするかを確認します
command + ` でターミナルを起動しカーソルをターミナルに移動した後
command + K でAI簡易モードを開き下記プロンプトを入力してください
@Vitest FizzBuzz.spec.tsのテストを実行してください
するとターミナルに実行コマンドが生成されます。
この機能は直感的かつ、普段ググって調べてる身からすると非常に便利です。
npx vitest run src/components/__tests__/FizzBuzz.spec.ts
npx vitest run src/components/__tests__/FizzBuzz.spec.ts✓ src/components/__tests__/FizzBuzz.spec.ts (1)
✓ FizzBuzz component (1)
✓ renders FizzBuzz correctly
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 11:36:15
Duration 607ms (transform 52ms, setup 0ms, collect 79ms, tests 12ms, environment 302ms, prepare 34ms)
AIの力だけでテストをパスすることができました。
修正
コンポーネントの内のnumbersを参照してFizzBuzzの結果を出力するのではコンポーネントとしての役割が薄いので、numbersをpropsとして渡すように修正します。
テストコードを修正する
テスト駆動開発なのでテストコードから修正します。
- /src/components/__ tests __/FizzBuzz.spec.ts
@Vitest @Vue FizzBuzz.spec.tsをpropsで値が渡ってくる想定のテストコードを作成してください
@Vitest @Vue propsのnumbersを複数条件でテストしたい
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import FizzBuzz from '../FizzBuzz.vue'
describe('FizzBuzz component with various props', () => {
const testCases = [
{
numbers: [1, 2, 3, 4, 5, 15, 30],
expected: ['1', '2', 'Fizz', '4', 'Buzz', 'FizzBuzz', 'FizzBuzz'],
},
{
numbers: [6, 10, 20, 21, 25, 45],
expected: ['Fizz', 'Buzz', 'Buzz', 'Fizz', 'Buzz', 'FizzBuzz'],
},
{
numbers: [7, 8, 9, 10, 11, 12],
expected: ['7', '8', 'Fizz', 'Buzz', '11', 'Fizz'],
},
]
testCases.forEach(({ numbers, expected }, index) => {
it(`renders correctly for test case ${index + 1}`, () => {
const wrapper = mount(FizzBuzz, {
props: {
numbers,
},
})
const paragraphs = wrapper.findAll('p')
expect(paragraphs.length).toBe(numbers.length)
paragraphs.forEach((p, i) => {
expect(p.text()).toBe(expected[i])
})
})
})
})
propsの値を考慮していないコンポーネントに対してテストを実行しているので当然テストでREDが発生します。
下記実行結果はテストコードのソースは正常に動いているので問題ありません。
case1はコンポーネント内のnumbersと値が一致するのでテストはパスします。
❯ src/components/__tests__/FizzBuzz.spec.ts (3)
❯ FizzBuzz component with various props (3)
✓ renders correctly for test case 1
× renders correctly for test case 2
× renders correctly for test case 3
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL src/components/__tests__/FizzBuzz.spec.ts > FizzBuzz component with various props > renders correctly for test case 2
FAIL src/components/__tests__/FizzBuzz.spec.ts > FizzBuzz component with various props > renders correctly for test case 3
AssertionError: expected 7 to be 6 // Object.is equality
- Expected
+ Received
- 6
+ 7
コンポーネントを修正する
テストをクリアするようにコンポーネントを修正します
- /src/components/FizzBuzz.vue
- /src/components/__ tests __/FizzBuzz.spec.ts
@Vue @TypeScript テストをクリアできるようにコンポーネントを修正してください
<template>
<div>
<p v-for="n in numbers" :key="n">{{ fizzBuzz(n) }}</p>
</div>
</template>
<script setup lang="ts">
// propsとしてnumbersを受け取る
const props = defineProps<{
numbers?: number[]
}>()
// propsを使用していることを明示
const { numbers } = props
// FizzBuzzのロジックを実装
function fizzBuzz(n: number): string {
if (n % 15 === 0) return 'FizzBuzz'
if (n % 3 === 0) return 'Fizz'
if (n % 5 === 0) return 'Buzz'
return n.toString()
}
</script>
✓ src/components/__tests__/FizzBuzz.spec.ts (3)
✓ FizzBuzz component with various props (3)
✓ renders correctly for test case 1
✓ renders correctly for test case 2
✓ renders correctly for test case 3
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 13:03:16
Duration 604ms (transform 52ms, setup 0ms, collect 77ms, tests 15ms, environment 307ms, prepare 39ms)
テストオールグリーンは達成しました。
ですが、画面表示にはまだ問題があります。
App.vueで何も出力されていません。
FizzBuzzを画面に出力します
propsの値を渡していないのでコンポーネントのdefinePropsのデフォルト値が使用されています。
親からpropsを渡してあげたコンポーネントを2つほど追加修正してみます。
ベースが完成しているので、Docを指定しなくてもコードを正常に生成してくれます。
- src/App.vue
FizzBuzzのコンポーネントを二つ設置してください
<template>
<v-app>
<v-main>
<v-container>
<FizzBuzz :numbers="[1, 2, 3, 4, 5, 15, 30]" />
<FizzBuzz :numbers="[6, 7, 8, 9, 10, 11, 12]" />
</v-container>
</v-main>
</v-app>
</template>
<script setup lang="ts">
import FizzBuzz from './components/FizzBuzz.vue'
</script>
<style>
@import 'vuetify/styles';
</style>
まとめ
今回なぜテスト駆動を採用したかというと、AIによる開発においてスタートとゴールを明確にし用意する事で実装ではAIが最短距離でゴールに到達する事ができる為です。
また、こちらの指示としても「テストをクリアできるように作成してください」だけでAIが生成してくれます。
実装から取り組んだ場合はAIに指示し逐一修正しながらゴールを目指す必要があるので、効率が悪いですし品質も落ちます。
感想
AiderやCopilotなどのAIコード補助ツールと比較してみると、扱いやすさは段違いに良いです。
ターミナルに対してAIを起動できたり、ドキュメントを指定してコードを生成できるのは非常に便利です。
逆にAiderのようなCLIツールと比較するとフルオートメーションはできないので、コードを生成した後に手動で修正する必要があります。
作業効率は格段に上げられるので、Cursorでフルオートメーション環境のベースを作成し、Aiderのようなツールでフルオートメーション管理する体制を作流のが理想系になります
Discussion