生成AIに設計・実装・試験を丸投げしたら、どこまでできるのか

# 目的
生成AIに設計から試験までを依頼した場合、どの程度の成果物が作成されるのかを検証します。

試しに今回はPythonで動作する計算機アプリを生成AIに作ってもらうことにしました。

今回は、出力内容の品質評価は行わず、成果物が一通り揃うかどうかに焦点を当てます。

# LLM
GPT-4o (2025/02/18 時点)

# 設計
## 入力プロンプト
```
Pythonで動く計算機を作りたいです。
ざっくり要件書くので、まずは設計をお願いします。

■動作環境
・Windows10
・Python3.10

■やりたいこと
・四則演算が行えるGUIの計算機
・テンキーからも入力可能

■設計の要件
・markdown形式の1ファイルで出力
```

## 出力
``` markdown
# 計算機アプリ設計書

## 1. 概要
Pythonで動作するGUI計算機アプリを作成する。基本的な四則演算をサポートし、テンキーからの入力にも対応する。

## 2. 動作環境
- OS: Windows 10
- Python: 3.10
- GUIライブラリ: Tkinter

## 3. 機能一覧
### 3.1 基本機能
- 数値入力
- 四則演算（加算、減算、乗算、除算）
- 計算結果の表示
- クリア機能
- テンキー入力対応

## 4. UI設計
### 4.1 画面構成
- **表示エリア**: 計算式と結果を表示するラベル
- **ボタンエリア**:
  - 数字ボタン（0～9）
  - 演算子ボタン（+、-、×、÷）
  - クリアボタン（C）
  - 計算実行ボタン（=）

### 4.2 キーボード入力対応
- 数字キー（0-9）を押すと入力される
- Enterキーで計算実行
- Backspaceキーで1文字削除
- Escapeキーで全クリア

## 5. クラス設計
CalculatorApp (Tk)
├── __init__(): GUIの初期化
├── create_widgets(): ウィジェットの作成
├── on_button_click(value): ボタンクリック処理
├── on_key_press(event): キーボード入力処理
├── calculate(): 計算実行
├── clear(): 入力クリア

## 6. 処理フロー
1. **起動**: アプリを起動し、ウィンドウを表示
2. **入力**: ボタンまたはテンキーで数値・演算子を入力
3. **計算**: `=` ボタンまたは Enter キーで計算実行
4. **結果表示**: 計算結果を画面に表示
5. **クリア**: `C` ボタンまたは Escape キーで入力クリア

## 7. 使用ライブラリ
- `tkinter`: GUI作成用
- `re`: 数式のバリデーション

## 8. 追加機能（将来的な拡張）
- 履歴機能
- 括弧演算のサポート
- 計算結果のコピー機能
- 設定画面（テーマ変更）

## 9. 開発スケジュール
| 期間 | 作業内容 |
|------|----------|
| 1日目 | 設計・環境構築 |
| 2日目 | UIの実装（ボタン・ラベル） |
| 3日目 | イベント処理（クリック・キー入力） |
| 4日目 | 計算処理・バリデーション実装 |
| 5日目 | 動作確認・デバッグ |

```
超ざっくりしかプロンプト書いていないので、基本設計と詳細設計の成果物が混ざった感じのものになってますね。

また、興味深かったのは、開発スケジュールまで出力された点です。
プロンプトでは「設計」を求めただけなのに、AIが「設計後の工程」まで考えたのはなぜでしょうか？

おそらく、生成AIは 「設計フェーズでは開発スケジュールをセットで作成するもの」 と学習しているのではないかと推測されます。

ただ、要件定義の段階でスケジュールを決めるのは一般的ではないため、違和感がある というのが正直なところです。


# 実装
## 入力プロンプト
```
続いて、この設計書をもとに、pythonプログラムの実装をお願いします。
```
## 出力
``` python
import tkinter as tk
from tkinter import ttk

class CalculatorApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("計算機")
        self.geometry("300x400")
        self.expression = ""
        self.create_widgets()
        self.bind_keys()

    def create_widgets(self):
        self.display = ttk.Label(self, text="", anchor="e", font=("Arial", 20))
        self.display.pack(expand=True, fill="both")
        
        buttons = [
            ('7', '8', '9', '/'),
            ('4', '5', '6', '*'),
            ('1', '2', '3', '-'),
            ('0', 'C', '=', '+')
        ]
        
        for row_values in buttons:
            row = tk.Frame(self)
            row.pack(expand=True, fill="both")
            for value in row_values:
                button = ttk.Button(row, text=value, command=lambda v=value: self.on_button_click(v))
                button.pack(side="left", expand=True, fill="both")

    def bind_keys(self):
        self.bind("<Return>", lambda e: self.calculate())
        self.bind("<BackSpace>", lambda e: self.clear_last())
        self.bind("<Escape>", lambda e: self.clear())
        for i in range(10):
            self.bind(str(i), lambda e, v=str(i): self.on_button_click(v))
        for op in ['+', '-', '*', '/']:
            self.bind(op, lambda e, v=op: self.on_button_click(v))
    
    def on_button_click(self, value):
        if value == "C":
            self.clear()
        elif value == "=":
            self.calculate()
        else:
            self.expression += value
            self.update_display()
    
    def update_display(self):
        self.display.config(text=self.expression)
    
    def calculate(self):
        try:
            result = eval(self.expression)
            self.expression = str(result)
            self.update_display()
        except Exception:
            self.expression = "Error"
            self.update_display()
    
    def clear(self):
        self.expression = ""
        self.update_display()
    
    def clear_last(self):
        self.expression = self.expression[:-1]
        self.update_display()

if __name__ == "__main__":
    app = CalculatorApp()
    app.mainloop()

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479797/5ae075ca-f25f-42ad-a25d-23a932038540.png)
それっぽいものができました。
# 試験
まずは試験項目を作ってもらいます
## 入力プロンプト
```
続いて、試験項目表の作成をお願いします。
試験項目は、設計書の内容を網羅するように作成してください。
```

## 出力
```
# 試験項目表

## 1. 基本動作確認
| No | 試験項目 | 入力 | 期待結果 |
|----|--------|----|----------|
| 1  | アプリ起動 | - | ウィンドウが表示される |
| 2  | 表示エリアの初期状態 | - | 何も表示されていない |
| 3  | 数値入力（ボタン） | 1, 2, 3 | "123" と表示される |
| 4  | 数値入力（テンキー） | 4, 5, 6 | "456" と表示される |
| 5  | 演算子入力（ボタン） | +, -, *, / | 演算子が表示される |
| 6  | 演算子入力（テンキー） | +, -, *, / | 演算子が表示される |
| 7  | クリアボタンの動作 | C | 表示がクリアされる |
| 8  | バックスペースキーの動作 | Backspace | 直前の入力が削除される |
| 9  | 計算実行（ボタン） | 3+2= | "5" と表示される |
| 10 | 計算実行（Enterキー） | 7-3 (Enter) | "4" と表示される |
| 11 | 計算エラーの表示 | 5/0 | "Error" と表示される |

## 2. 追加動作確認
| No | 試験項目 | 入力 | 期待結果 |
|----|--------|----|----------|
| 12 | 連続計算 | 2+3=+4= | "9" と表示される |
| 13 | クリア後の動作 | 123 C 5+5= | "10" と表示される |
| 14 | 計算途中でのクリア | 9+3 C | 表示がクリアされる |
| 15 | 計算途中での削除 | 8+5 Backspace | "8+" と表示される |
| 16 | 長い数式の入力 | 12345+67890= | "80235" と表示される |

## 3. キーボード入力確認
| No | 試験項目 | 入力 | 期待結果 |
|----|--------|----|----------|
| 17 | 数字キーの動作 | 0-9 | 入力が表示される |
| 18 | 演算キーの動作 | +, -, *, / | 演算子が表示される |
| 19 | Enterキーの動作 | 10+5 (Enter) | "15" と表示される |
| 20 | Escapeキーの動作 | 数式入力後 (Escape) | 表示がクリアされる |

## 4. 例外処理確認
| No | 試験項目 | 入力 | 期待結果 |
|----|--------|----|----------|
| 21 | 無効な計算の処理 | 10//2 | "Error" と表示される |
| 22 | 空の状態で計算 | (Enter) | 何も変化しない |
| 23 | 数式が不完全な場合 | 5+ (Enter) | "Error" と表示される |

## 5. UIの動作確認
| No | 試験項目 | 操作 | 期待結果 |
|----|--------|----|----------|
| 24 | ウィンドウサイズの変更 | マウスでリサイズ | 正しくリサイズされる |
| 25 | ボタンのクリック範囲 | ボタンをクリック | 入力が正しく処理される |

```
試験項目表ができたので、テストコードも作成してもらいます。
## 入力プロンプト
```
先ほどの試験項目表を消化するテストコードを作成してください。
テストコードは、試験項目ごとに関数化し、関数名には試験項目表のNoを含めるようにしてください。
```
## 出力
``` python
import unittest
from tkinter import Tk

class TestCalculatorApp(unittest.TestCase):
    def setUp(self):
        self.app = CalculatorApp()
        self.app.update()
    
    def tearDown(self):
        self.app.destroy()
    
    def test_01_app_startup(self):
        self.assertIsNotNone(self.app)
    
    def test_02_initial_display_empty(self):
        self.assertEqual(self.app.expression, "")
    
    def test_03_number_input_buttons(self):
        for num in "123":
            self.app.on_button_click(num)
        self.assertEqual(self.app.expression, "123")
    
    def test_04_number_input_tenkey(self):
        for num in "456":
            self.app.on_button_click(num)
        self.assertEqual(self.app.expression, "456")
    
    def test_05_operator_input_buttons(self):
        for op in "+-*/":
            self.app.on_button_click(op)
        self.assertEqual(self.app.expression, "+-*/")
    
    def test_06_operator_input_tenkey(self):
        for op in "+-*/":
            self.app.on_button_click(op)
        self.assertEqual(self.app.expression, "+-*/")
    
    def test_07_clear_button(self):
        self.app.on_button_click("C")
        self.assertEqual(self.app.expression, "")
    
    def test_08_backspace_function(self):
        self.app.on_button_click("8")
        self.app.on_button_click("5")
        self.app.clear_last()
        self.assertEqual(self.app.expression, "8")
    
    def test_09_calculation_button(self):
        self.app.on_button_click("3")
        self.app.on_button_click("+")
        self.app.on_button_click("2")
        self.app.on_button_click("=")
        self.assertEqual(self.app.expression, "5")
    
    def test_10_calculation_enter_key(self):
        self.app.on_button_click("7")
        self.app.on_button_click("-")
        self.app.on_button_click("3")
        self.app.calculate()
        self.assertEqual(self.app.expression, "4")
    
    def test_11_divide_by_zero(self):
        self.app.on_button_click("5")
        self.app.on_button_click("/")
        self.app.on_button_click("0")
        self.app.on_button_click("=")
        self.assertEqual(self.app.expression, "Error")
    
    def test_12_sequential_calculations(self):
        for char in "2+3=+4=":
            self.app.on_button_click(char)
        self.assertEqual(self.app.expression, "9")
    
    def test_13_clear_after_input(self):
        self.app.on_button_click("123")
        self.app.on_button_click("C")
        self.app.on_button_click("5")
        self.app.on_button_click("+")
        self.app.on_button_click("5")
        self.app.on_button_click("=")
        self.assertEqual(self.app.expression, "10")
    
    def test_14_clear_midway(self):
        self.app.on_button_click("9")
        self.app.on_button_click("+")
        self.app.on_button_click("3")
        self.app.on_button_click("C")
        self.assertEqual(self.app.expression, "")
    
    def test_15_delete_midway(self):
        self.app.on_button_click("8")
        self.app.on_button_click("+")
        self.app.on_button_click("5")
        self.app.clear_last()
        self.assertEqual(self.app.expression, "8+")
    
    def test_16_long_expression(self):
        for char in "12345+67890=":
            self.app.on_button_click(char)
        self.assertEqual(self.app.expression, "80235")
    
    def test_21_invalid_expression(self):
        self.app.on_button_click("+")
        self.app.on_button_click("=")
        self.assertEqual(self.app.expression, "Error")
    
    def test_23_incomplete_expression(self):
        self.app.on_button_click("5")
        self.app.on_button_click("+")
        self.app.on_button_click("Enter")
        self.assertEqual(self.app.expression, "Error")
    
    def test_24_window_resize(self):
        self.app.geometry("400x500")
        self.assertEqual(self.app.winfo_width(), 400)
        self.assertEqual(self.app.winfo_height(), 500)
    
if __name__ == "__main__":
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
```
## 結果
```
.................FF
======================================================================
FAIL: test_23_incomplete_expression (__main__.TestCalculatorApp)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\tmp\ipykernel_492\4019986983.py", line 111, in test_23_incomplete_expression
    self.assertEqual(self.app.expression, "Error")
AssertionError: '5+Enter' != 'Error'
- 5+Enter
+ Error


======================================================================
FAIL: test_24_window_resize (__main__.TestCalculatorApp)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\tmp\ipykernel_492\4019986983.py", line 115, in test_24_window_resize
    self.assertEqual(self.app.winfo_width(), 400)
AssertionError: 300 != 400

----------------------------------------------------------------------
Ran 19 tests in 3.000s

FAILED (failures=2)
```
No23, 24が失敗していますね。
## 生成AIにエラーメッセージを入力
```
テストの失敗を修正するために、以下の2点を修正します。

修正点
test_23_incomplete_expression

現在のコードでは "Enter" という文字列が self.app.expression に追加されてしまっている。
Enter キーは self.app.calculate() を実行するように修正。
test_24_window_resize

self.app.geometry("400x500") を設定しても、すぐに self.app.winfo_width() を取得すると反映されない場合がある。
self.app.update_idletasks() を呼び出して、UIの更新を確実に反映させる。
修正後のコード
以下の修正を適用します。

    def test_23_incomplete_expression(self):
        self.app.on_button_click("5")
        self.app.on_button_click("+")
        self.app.calculate()
        self.assertEqual(self.app.expression, "Error")
    
    def test_24_window_resize(self):
        self.app.geometry("400x500")
        self.app.update_idletasks()
        self.assertEqual(self.app.winfo_width(), 400)
        self.assertEqual(self.app.winfo_height(), 500)
    

修正を適用しました。テストを再実行して、期待通り動作するか確認してください。問題があれば教えてください。
```

## 結果
```
...................
----------------------------------------------------------------------
Ran 19 tests in 2.909s

OK
```
一見問題ないように見えますが、試験項目が24なのに対して、19しかテストが行われていないのはおかしいですね。

改めて確認すると、No17~20のテストコードがありません。
出力されなかった仮説をいくつか考えてみました。
 - No17~20はキーボード入力に関するテストなので、自動化が難しいと判断し省略した
 - 本当は実装したかったが、TkinterのGUI操作を行うテストコードは作成が困難だった
 - 純粋に抜け落ちた
生成AIが 「テストコードを書くべきかどうか」を判断する基準 を知るためにも、もう少し深掘りしたいところですが、今回はここまでの検証としておきます。

# まとめ
生成AIを利用することで、設計～試験までの成果物を一通り作成することができることを確認できました。

ただし、今回の検証では、プロンプトが簡易的なものだったこともあり、設計の粒度がバラバラだったり、試験項目がすべてカバーされていなかったりと、いくつかの課題も見えました。

今回は出力内容の品質評価までは行っていませんが、
「生成AIはどこまで一貫した設計・実装・試験を作成できるのか？」 をより深掘りする余地はありそうです。

## 今後の検証ポイント
- 設計や試験工程を細分化
- 設計工程で複数ドキュメントを作成したうえでの製造、試験が行えるか
- 試験項目が設計と適切に対応しているかの妥当性確認
- テストコードが試験項目表に準じたものになっているかの確認