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」を発言
- 間違えた人から脱落

プログラミングなので１人でやり、脱落もしませんのでプログラミングの仕様に落とし込むと下記のようになります

- 3で割り切れるときは「Fizz」を出力
- 5で割り切れるときは「Buzz」を出力
- 両方で割り切れるときは「FizzBuzz」を出力
- それ以外の場合は数字を出力

## 参考資料
[ますみ / 生成AIエンジニア](https://zenn.dev/umi_mori/books/ai-code-editor-cursor)の著書  
[サクッと始めるAIコードエディタ【Cursor / VS Code / ChatGPT】](https://zenn.dev/umi_mori/books/ai-code-editor-cursor)

無料で読めるのでぜひ読んでみてください  
上記本を参考に今回のハンズオン形式の記事を作成しました

## インストール

CursorはVSCodeの拡張機能として活用できるので、VSCodeをインストールしている場合は拡張機能をそのまま活用することができます。

[公式サイト](https://www.cursor.com/)

## 開発をしてみる

- Vue.js
- TypeScript
- Vuetify
- Vite
- Vitest

上記環境でプロジェクトを作成してみます。

```bash
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　← この技術はデフォルトで設定されている
- TypeScript
- Vuetify ← この技術は何故かデフォルトで設定されている
- Vitest

**command + K** でDocsを開き@docsを入力してください  
候補の中からDocsを選択してください  
**+Add new docs**を選択して下記ドメインをドキュメントに追加してください

![277e75e9b3df27-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3822984/708aa934-2dc4-9500-fd16-e9fd89b47a55.png)

- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
- [Vitest Documentation](https://vitest.dev/guide/)

## Vuetifyのインストール

```bash
npm install vuetify@next
```

main.tsにVuetifyをインストールします。  
AInに質問することなく、Cursorの自動補完によって下記コードを生成しています。

```typescript:main.ts
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です

![277e75e9b3df27-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3822984/99a9a99f-2b21-75cc-9ecd-cbf9022d80b7.png)

```
@Vitest FizzBuzzのテストコードを作成してください
```

![277e75e9b3df27-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3822984/fd222e4d-1621-5d95-97e8-2dbcafa2f815.png)


**Apply**を押してファイルに反映させる事ができます

このコードはFizzBuzzコンポーネントを作成していないのでエラーが出ますが一旦このままにしておきます。  
コンポーネントを作成時に修正するようにします

:::note warn
テストファイルのプロンプトの生成時にテストのコメントを日本語生成するように命令しておくべきでした。  
上記と同じ手順で修正も可能なので気になる人はしてしまいましょう
:::

## FizzBuzzコンポーネントを作成する
下記プロンプトでFizzBuzzコンポーネントを作成しました  
対象ファイルを下記二つの複数を選択しました

- /src/components/FizzBuzz.vue
- /src/components/__ tests __/FizzBuzz.spec.ts

```
@Vue @TypeScript @Vuetify   Compositionを利用してFizzBuzz.spec.tsをオールグリーンになるコンポーネントを作成してください
```

```vue:FizzBuzz.vue
<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>
```

```typescript:FizzBuzz.spec.ts
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は簡素にしておきます

```vue:App.vue
<script setup lang="ts"></script>

<template>
  <main></main>
</template>
```

### プロンプトでコードを生成してみる

```
@Vue @TypeScript @Vuetify 画面にFizzBuzzコンポーネントを表示してください
```

```vue:App.vue
<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>
```

```bash
# 実行確認
npm run dev
```

シンプルですが概ね期待通りの結果が出力されました  
![277e75e9b3df27-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3822984/7b750aeb-10f7-6a8e-241a-f2eb79049927.png)


## テスト結果の確認
画面上問題なく見えるかテストをパスするかを確認します

**command + `** でターミナルを起動しカーソルをターミナルに移動した後  
**command + K** でAI簡易モードを開き下記プロンプトを入力してください

```
@Vitest FizzBuzz.spec.tsのテストを実行してください
```

するとターミナルに実行コマンドが生成されます。  
この機能は直感的かつ、普段ググって調べてる身からすると非常に便利です。

```bash
npx vitest run src/components/__tests__/FizzBuzz.spec.ts
```

```bash
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を複数条件でテストしたい
```

```typescript:FizzBuzz.spec.ts
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と値が一致するのでテストはパスします。

```bash
❯ 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 テストをクリアできるようにコンポーネントを修正してください
```

```vue:FizzBuzz.vue
<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>
```

```bash
✓ 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を渡してあげたコンポーネントを２つほど追加修正してみます。

ベースが完成しているので、Docを指定しなくてもコードを正常に生成してくれます。

- src/App.vue

```
FizzBuzzのコンポーネントを二つ設置してください
```

```vue:App.vue
<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のようなツールでフルオートメーション管理する体制を作流のが理想系になります

## 一緒に働く仲間を募集しています！
株式会社コネクター・ジャパンでは一緒に働いてくれる仲間を募集しています！

事業拡大に伴い、エンジニアを大募集しています。
興味のある方は下記リンクから弊社のことをぜひ知っていただき応募してもらえると嬉しいです。
▼会社について
https://www.wantedly.com/companies/cnctor/about
▼代表メッセージ
https://cnctor.jp/10years-anniversary/
▼応募はこちら
https://www.wantedly.com/companies/cnctor/projects