EY-Office ブログ

Spec Driven Development入門で作ったECサイトを完成してみた

遅ればせながらSpec Driven Developmentに入門してみた(1)(2)(3)でClaude Codeに設計してもらったワインのECサイトの全ての実装をClaude Codeに行ってもらいました。

SpecDrivenDevelopment Bing Image Creatorが生成した画像を使っています

実装

遅ればせながらSpec Driven Developmentに入門してみた(3)では商品(ワイン)の一覧が表示された段階で終了してしまいましたので続きの実装をを頼みました。

グループ完了残り状況
1. プロジェクト基盤2/20✅ 完了
2. データモデル1/10✅ 完了
3. ユーザー認証(BE)0/22❌ 未着手
4. 商品カタログ(BE)2/20✅ 完了
5. メール通知0/11❌ 未着手
6. フロントエンド認証基盤0/33❌ 未着手
7. 商品カタログ(FE)2/20✅ 完了
8. カート(BE)0/22❌ 未着手
9. カート(FE)0/11❌ 未着手
10. 注文・決済(BE)0/22❌ 未着手
11. 注文・決済(FE)0/22❌ 未着手
12. 管理 DB(BE)0/11❌ 未着手
13. 管理 DB(FE)0/33❌ 未着手
14. シードデータ1/10✅ 完了
15. 結合/E2Eテスト0/33❌ 未着手
16. お薦めワイン機能3/30✅ 完了

(BE)はバックエンド、(FE)はフロントエンドです

/kiro:spec-impl wine-ec-site 3のような実装コマンド実行するとTDDで、テストコードが生成されテストを実行しながらコードの実装が進んで行きます。
実装の時間は5時間くらいだったでしょうか? Claude CodeのContextが一杯になり2度くらい/clearを実行しました。

さてここで、設計書(design.md) にはプログラムの起動方法やテスト方法が書かれていない事に気がつき、以下のように指示してREADME.mdを作ってもらいました。

README.md ファイルを作ってください。内容は、

- このアプリの概要
- アプリの起動方法
- テストの実行方法
   - バックエンド、フロントエンド、E2E 全て

いよいよ実行

README.mdをみながら以下の手順でバックエンド、フロントエンドを起動しブラウザーでアクセスしました。

$ vi backend/.env                                    ← データベース、決済等の設定

$ docker compose up -d                               ← PostgreSQL、バックエンドの起動
$ docker compose exec backend alembic upgrade head   ← DBマイグレーションの実行
$ docker compose exec backend python -m app.seed     ← シードデータの投入
$ cd frontend
$ npm install
$ npm run dev                                        ← フロントエンドの起動

1. 表示

http://localhost:5173/をアクセスするとおすすめワインが表示されます。

商品をクリックすると商品が表示されます。

2. カートページ失敗

カートに追加ボタンをクリックするとログイン画面が表示されます、ログインしてないとカートに追加できない仕様だったのですね(いま気がつきました)。それからログイン画面のデザインが微妙ですね😅。

しかし、カートページには遷移しません! カートページのURL http://localhost:5173/cartを入力してアクセスすると、またログイン画面が表示されます。何か変ですね❓

3. そうだテストを実行してみよう

ついつい何時ものようにブラウザーの開発ツールのNetworkタブを使って通信内容をみたりするとログイン・セッションの管理が出来てないようです。
ここでテストの事を思い出したので、テストを実行してみました。

$ cd backend
$ pytest -v
================== test session starts ==================

・・・

tests/test_admin_api.py::TestAdminCreateWine::test_admin_can_create_wine PASSED                                                                                         [  0%]
tests/test_admin_api.py::TestAdminCreateWine::test_regular_user_cannot_create_wine_403 PASSED                                                                           [  0%]
tests/test_admin_api.py::TestAdminCreateWine::test_unauthenticated_cannot_create_wine_401 PASSED                                                                        [  1%]
tests/test_admin_api.py::TestAdminCreateWine::test_invalid_payload_returns_422 PASSED                                                                                   [  1%]
tests/test_admin_api.py::TestAdminUpdateWine::test_admin_can_update_wine PASSED                                                                                         [  2%]
tests/test_admin_api.py::TestAdminUpdateWine::test_regular_user_cannot_update_wine_403 PASSED                                                                           [  2%]
tests/test_admin_api.py::TestAdminUpdateWine::test_update_nonexistent_wine_returns_404 PASSED                                                                           [  3%]

・・・

===================== 223 passed in 35.25s ===================

$ cd ../frontend
$ npm test

・・・

 ✓ src/test/WineDetailPage.test.tsx (15 tests) 416ms
 ✓ src/test/CartPage.test.tsx (11 tests) 472ms
 ✓ src/test/LoginPage.test.tsx (6 tests) 500ms
 ✓ src/test/WineListPage.test.tsx (12 tests) 510ms
 ✓ src/test/CheckoutPage.test.tsx (9 tests) 720ms
 ✓ src/test/RegisterPage.test.tsx (7 tests) 785ms
 ✓ src/test/RecommendedPage.test.tsx (7 tests) 178ms
 ✓ src/test/OrderPages.test.tsx (12 tests) 171ms
 ✓ src/test/Header.test.tsx (8 tests) 106ms

・・・

 Test Files  14 passed (14)
      Tests  119 passed (119)

$ npm run test:e2e

・・・

  ✓  11 [chromium] › e2e/purchase.spec.ts:25:3 › 購入フロー › ログインページが表示されること (183ms)12 [chromium] › e2e/purchase.spec.ts:33:3 › 購入フロー › ログインに成功すること (2.3s)13 [chromium] › e2e/purchase.spec.ts:46:3 › 購入フロー › 未ログイン時にカートページへアクセスするとログインへリダイレクトされること (186ms)14 [chromium] › e2e/purchase.spec.ts:53:3 › 購入フロー › ログイン後にカートページへアクセスできること (3.3s)15 [chromium] › e2e/purchase.spec.ts:69:3 › 購入フロー › カートページに合計金額が表示されること (5.8s)

・・・

E2Eテストでもログイン後にカートページにアクセス出来ない事が判ったので、詳細なエラーと共にClaude Codeに指示して修正してもらいました(フロントエンドに認証済みかチェックするAPI呼出しが追加されました)。

> E2Eテスト ログイン後にカートページへアクセスできること がエラーになります修正してください。

 1) [chromium] › e2e/purchase.spec.ts:53:3 › 購入フロー › ログイン後にカートページへアクセスできるこ
    Error: expect(page).not.toHaveURL(expected) failed

  ・・・

3. カートページ成功

無事に修正できたようなので、確認したところカートが表示されました
また、画面ヘッダーにカートボタンが無かったので追加してもらいました。

4. テストコードがおかしい

どんなテストが行われているのだろうと思い、E2Eテストを見てみると以下のテストは カートページに合計金額が表示されること を確認するコードのはずですが、全く合計金額などチェックしていません❗❗

  test('カートページに合計金額が表示されること', async ({ page }) => {
    // ログインする
    await page.goto('/auth/login')
    await page.getByLabel(/メールアドレス|Email/i).fill(TEST_USER.email)
    await page.getByLabel(/パスワード/i).fill(TEST_USER.password)
    await page.getByRole('button', { name: /ログイン/ }).click()
    await page.waitForTimeout(2000)

    // カートページへアクセスする
    await page.goto('/cart')
    await page.waitForTimeout(1000)

    // 「購入手続きへ」ボタンまたは「カートが空」メッセージが表示されること
    const hasCheckoutButton = await page.getByRole('link', { name: /購入手続き/ }).count()
    const hasEmptyMessage = await page.getByText(/カートが空|商品がありません/).count()
    expect(hasCheckoutButton > 0 || hasEmptyMessage > 0).toBe(true)
  })

これも修正してもらいました。

  test('カートページに合計金額が表示されること', async ({ page }) => {
    // ログインする
    await page.goto('/auth/login')
    await page.getByLabel(/メールアドレス|Email/i).fill(TEST_USER.email)
    await page.getByLabel(/パスワード/i).fill(TEST_USER.password)
    await page.getByRole('button', { name: /ログイン/ }).click()
    await page.waitForTimeout(2000)

    // ワイン一覧ページへアクセスし、在庫の多い商品を選択する
    await page.goto('/wines')
    await page.waitForTimeout(1000)
    const searchInput = page.getByPlaceholder(/検索/)
    await searchInput.fill('マルベック・レゼルバ')
    await page.getByRole('button', { name: /検索/ }).click()
    await page.waitForTimeout(1000)
    const wineCard = page.locator('.card').filter({ hasText: /¥/ }).first()
    await wineCard.click()
    await page.waitForURL(/\/wines\/\d+/)

    // カートに追加する
    await expect(page.getByRole('button', { name: /カートに追加/ })).toBeVisible()
    await page.getByRole('button', { name: /カートに追加/ }).click()
    await expect(page.getByText(/カートに追加しました/)).toBeVisible()

    // カートページへアクセスする
    await page.goto('/cart')
    await page.waitForTimeout(1000)

    // 合計金額が表示されていることを確認する
    await expect(page.getByText(/合計/)).toBeVisible()
    // 合計金額の行に金額フォーマット(例: ¥1,000)が表示されること
    const totalRow = page.locator('div').filter({ hasText: /合計/ }).last()
    await expect(totalRow.locator('span').filter({ hasText: /[¥¥]\s?[\d,]+/ })).toBeVisible()
  })

5. 購入ページ

カートページにある購入手続きへボタンを押しても購入ページへ遷移しないので、これも修正してもらいまいました(やはり、ダサい画面ですね😅)。

6. カード決済

カード決済にはStripeを使っていますが、初めて使うので判らないことが多くClaudeに助けてもらいました。なんとかテスト用のカード番号でカード決済が出来るようになりました。

7. 管理画面

管理画面も最初はログインできませんでしたが、修正後は動いているようです。まだ機能は足りませんが・・・

まとめ

コードの大きさ

今回Claude Codeで生成されたコードの量は

  • バックエンド: 約6千行
    • Python: FastAPI, SQLAlchemy
  • フロントエンド: 約6千行
    • TypeScript: React, TanStack Router/Query, daisyUI+Tailwind CSS

現在サポートしている、あるECサイトはRuby on Railsで約2万5千行です。単純な比較はできませんが今回生成されたコードは、よくあるTODOアプリ等とは違い実用に近い大きさがあると思います。

得られた知見

必ず全テストを実行しよう

今回利用したSDDフレームワークcc-sddはTDD(テスト駆動開発)で開発を行いますが、なぜか開発時に通っているはずのテストを開発終了後に実行したら失敗していました。
開発が終了したら、まずは全テストを実行しましょう。

ただし、テストがあるので問題点も指定しやすくAIも解決しやすいと思います。AIに作らせる場合もテスト重要ですね❗

機能の接合部が弱いかな?

今回おきた問題は、機能の接合部で発生していました。
各機能に分割され、それぞれの機能はTDDで開発されますが、それらを繋ぐ部分はE2Eテストしかテスト出来ないので機能と機能の接合部は弱いのかも知れません。今回は開発後にいくつかのE2Eテストを追加しました。

テストコードはレビューしよう

今のところ生成されたコードやテストコード全体はコードレビューしてないのですが、少なくともテストコードはレビューした方が良いと思いました。記事に書いたように設計段階では適切なテストを設定していても実装がそれに伴っていないケースが在ったのは驚きでした❗ AIもサボるのですね。😅

Claudeの画面デザインは今一つ

今回のプロジェクトでは画面デザインに付いては指定しませんでしたが、やはり過去の実験同様にClaudeの作るデフォルトの画面デザインは今一つですね。😅
実際のプロジェクトでは画面デザインに関しての指定や他のツールを使った方が良いかもしれませんね。

まとめのまとめ

今回のプロジェクトではSpec Driven Development(SDD)を使う事でシステムの機能やアーキテクチャ等は適切に出来ました。しかし、コーディングレベルまで落ちてくると不具合が発生していました。少なくともテストコードのレビューは必要ですね。

まあ、従来の人間がシステムを作っていた頃も同じような問題は発生してたと思いますが、圧倒的なローコストでこのレベルのシステムが作れてしまうのは、時代が大きく変わった事を実感しました。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ