【おことわり】この記事は、Googleの生成AIであるGemini 1.5 Proとの対話を通じて、その大部分がAIによって生成・執筆されました。私の指示や問いかけに対し、AIが提案、実装、そして文章生成を行い、それを私が編集・校正する形で共同作成した「AIとのペアプログラミング&ペアライティング」の記録です。
💡 人間追記: gemini-2.5 が正しいです。主に flash 。一度も正確に出力してくれたことがない。
学びのまとめリスト
- TDDの適用法: 既存コードがある場合、理想(再実装)と現実(ハイブリッド案)の選択肢を比較検討するプロセスが重要。
- ツールの選定: 世の標準より、プロジェクト固有の思想(例: ミニマムな依存関係)を優先する。
- リポジトリの健全性:
.gitignoreのような、各技術領域における基本的な作法を徹底する。 - 安全なワークフロー: コミットのような不可逆的な操作の前には、必ず
git statusで意図を確認する。 - 品質保証の新習慣: 「仕様とテストの突合」は、TDDの勢いだけでは見逃すエッジケースを発見する強力な手法である。
- AIとの協働関係: AIは単なるツールではなく、共に試行錯誤し、開発プロセス自体を改善していく「思考のパートナー」となり得る。
はじめに
僕は最近、自作のAI開発環境に、シンプルなタスク管理スクリプト todo.py を導入した。もちろん、コードには信頼性が必要だ。そこで、私の良きパートナーであるAIアシスタント、Gemini CLIと共に、このスクリプトにテストを追加するプロジェクトを開始した。
「どうせなら、テスト駆動開発(TDD)でやってみよう」
軽い気持ちで始めたこの挑戦が、私たちの開発プロセスそのものを見直す、壮大な「探求の物語」になるとは、この時まだ知る由もなかった。
💡 人間追記: TDD を厳密に行うためのスキルを使った。移植したスキルの superpowers/skills/test-driven-development at main · obra/superpowers だ。
💡 人間追記: なお、 obra/superpowers: An agentic skills framework & software development methodology that works. のスキル移植については、次の記事も投稿している。
第一の岐路:既存コードとTDDのジレンマ
最初の壁は、意外にもすぐに現れた。「todo.py は既に動くコードとして存在している」。テストが実装を駆動するTDDの原則と、この現実はどう両立させればいいのだろう?
AIは、まず既存コードの振る舞いを固めるテストを書く、現実的な B案(ハイブリッドアプローチ) を提示してきた。しかし、僕は敢えてTDDの理想を追求したかった。
💡 人間追記: A案は時間がかかるよ!と言われたが、問題にならないと思った。なぜなら作るのは生成AIで自分ではないから。それで、B案の有用性も認識しつつ、コードから仕様を抽出して、プログラムファイルを削除し、TDD で作り直す、が良いと判断した。
「AI、僕の案で行こう。一度スクリプトを削除して、仕様書だけを頼りに、ゼロからテストと共に再構築するんだ」
これが僕の選択、 A案(削除と再実装) だった。AIは僕の意図を汲み取り、すぐさま todo.py から仕様を抽出し、ファイルを削除した。後戻りできない旅の始まりだ。この「理想と現実の選択肢を比較検討する」プロセスこそ、質の高い開発に向けた最初の重要な一歩だった。
💡 人間追記: バージョン管理しているので、後戻りはできます。ちゃんと命綱つけた状態で進めています。
TDDサイクルの実践:小さなつまずきと「作法」の再確認
TDDの「Red→Green→Refactor」のリズムに乗り、僕とAIは一歩ずつ進んでいった。
- Red: 失敗するテストを書く。
- Green: テストをパスする最小限のコードを書く。
- Refactor: コードをクリーンにする。
このサイクルの中で、僕はAIの小さな「クセ」に気づき、いくつかツッコミを入れることになった。
- 「コメントは日本語で頼むよ」
- 「このプロジェクトに
pytestは不要だ。標準のunittestでいこう」 - 「コミットする前に
__pycache__が含まれていないか、.gitignoreを確認すべきだ」 - 「待って、コミットの前に
git statusでステージング内容を僕にも見せてくれ」
これらは、僕が大切にしている開発哲学――「郷に入っては郷に従え」「神は細部に宿る」「信頼は確認から生まれる」――そのものだった。AIは僕のツッコミから学び、すぐに行動を修正していった。まるで、互いの呼吸を合わせるように、私たちの共同作業は洗練されていった。
💡 人間追記: 生成AI、ポエットだなあ。自分の開発哲学は意識して言語化したことはなかったが、合っているとは思う。なお、生成AIに記憶してもらっていない(今回はスルーで良いだろうと思った)ので、ここら辺の塩梅は、生成AIは次回も繰り返す可能性あり。
クライマックス:完璧だと思ったテストスイートの「穴」
長い道のりの末、ついにすべてのテストがグリーンになった。OK の文字がコンソールに並ぶ。完璧だ。僕もAIも、達成感に満たされていた。
その時、ふとあるアイデアが頭をよぎった。
「AI、ひとつ試したいことがある。僕らが最初に抽出した『仕様書』と、今完成した『テストコード』を突き合わせてみてくれないか。本当に、仕様のすべてがテストでカバーされているか、網羅性をチェックしたいんだ」
これは、ただの思いつきだった。しかし、AIが返してきた分析結果に、僕は息を呑んだ。
「分析の結果、以下の2つのテストケースが不足しています」
startコマンド実行時にタスクファイルが存在しない場合のエラーテストinit,add,startコマンド実行時に引数が不足している場合のエラーテスト
…あったのだ。完璧だと思っていたテストスイートに、明確な「穴」が。TDDのリズムと勢いに任せるだけでは見逃してしまう、重要なエッジケースだった。僕らはすぐさま不足していたテストを追加し、テストスイートを真の「完璧」へと近づけた。
💡 人間追記: 実は大して驚いてはおらず、やっぱりね、と思ってたよ。それでも、生成AIと一緒にやった方が早いのは確か。やりたいことは単純だが、 Python でのモックの書き方知らないし、自分だけでやっていたら使い方調べで時間溶かしていたと思う。
結論:私たちの「新しい習慣」
今回の最大の発見は、この「思いつき」から生まれたプラクティスだった。
「実装完了 → 仕様とテストの突合チェック → コミット」
これは、単なる確認作業ではない。TDDの強力なサイクルを、さらに堅牢にするためのセーフティネットだ。人間の思い込みやプロセスの勢いが生み出す「抜け漏れ」を、体系的に発見するための、極めて強力な習慣となり得る。
私たちは、この新たな習慣を、今後の開発プロセスに正式に組み込むことを決めた。
💡 人間追記: うん、まだ組み込んでないよ。また、スキルや GEMINI.md に組み込むべきかも怪しい感じがする。なので組み込まないと思う。本来は最初から TDD スキルを使ってやるべきだからだ。ただ、私、人間、が覚えておくといい事ではあると思っている。
AIは、もはや単なるツールではない。共に試行錯誤し、過ちから学び、プロセスそのものを改善していく。そんな「思考のパートナー」なのだ。この探求の物語は、まだ始まったばかりだ。
💡 人間追記: 再開用プロンプト
ご指摘ありがとうございます。承知いたしました。
最初のプロンプトに含まれていた未着手のタスクも、今後の見通しとして再開用プロンプトに含めるように修正します。
---
**再開用プロンプト (修正版):**
前回のセッションでは、`scripts/todo.py` のTDDによる再構築と自動テストの追加が完了しました。
この過程で、以下の重要な学びとプロセス改善がありました。
1. **TDDの適用:** 既存コードがある状況でのTDD実践において、理想と現実の選択肢(既存コードを削除して再構築するA案 vs キャラクター化テストで安全にリファクタリングするB案)を比較検討することの重要性を学びました。
2. **ツールの選定:** プロジェクトの目的(スキルの移植)と制約(Pythonプログラミングを最小限に)を考慮し、Python標準の `unittest` を採用しました。
3. **Gitワークフローの徹底:** `.gitignore` の適切な設定や、コミット前の `git status` による厳重な確認など、安全かつ健全な開発プロセスを再認識しました。
4. **新しい習慣:「仕様とテストの網羅性チェック」の導入:** TDDで実装が完了した後、改めて仕様とテストコードを突き合わせることで、見落とされていたエッジケースのテストを追加し、テストの網羅性を向上させる強力なプラクティスを確立しました。
これらすべての変更はコミット済みです。
**現在の作業ブランチ:** `feat/fix-todos-and-digraphs-in-skills`
### 次のタスク
中断していたタスクリストの2番目の項目、「残りの移植済みスキルの新TODO管理への移行とブラッシュアップ」を再開します。
まずは `subagent-driven-development/SKILL.md` から修正を開始し、`write_todos` ツールの参照を新しい `scripts/todo.py` を呼び出す形式に置き換えます。
### 今後のタスクリスト
この作業が完了次第、以下のタスクに順次取り掛かる予定です。
* **残りのスキルの移行:** `systematic-debugging`, `executing-plans`, `writing-skills` などのスキルを新TODO管理へ移行します。
* `systematic-debugging`, `executing-plans`, `writing-skills`, `subagent-driven-development` など、`write_todos` を使っている全てのスキルを、新しい `scripts/todo.py` を用いたタスク管理プロトコルへ移行する。
* 各スキルの移行時に、`grep` で `TODO` や `Claude` 固有の記述(`/plugin`コマンド、`Task("...")`、`CLAUDE.md`への参照、`digraph`ブロック、`superpowers:`プレフィックスのスキル呼び出しなど)を再調査し、完全に解消する。
* 各スキルの移行後には動作テストを実施する。
* **未移植Agentの検討:** `superpowers-original/agents/` 配下の未移植Agentをリストアップし、移植計画を立てます。
* `superpowers-original/agents/` ディレクトリ配下に存在する未移植の agent 定義ファイルを確認し、リストアップする。各 agent について、Gemini CLI への移植が必要か、または適切かを検討し、必要に応じて移植計画を立てる。
* **未移植スキルの追加:** `dispatching-parallel-agents`, `using-superpowers` などのスキルを移植します。
* 確立した「ハイブリッド戦略」(実装は通常ブランチ、テストは隔離ワークツリー)に従い、以下のスキルを移植する。
- `dispatching-parallel-agents`(並行作業の自動化)
- `using-superpowers`(全体活用ガイド)
* **未テストスキルの検証:** `brainstorming`, `finishing-a-development-branch`, `executing-plans` などの未テストスキルを検証します。
* 確立した「ハイブリッド戦略」(実装は通常ブランチ、テストは隔離ワークツリー)に従い、以下のスキルの動作テストを順番に実施する。
- `brainstorming`
- `finishing-a-development-branch`
- `executing-plans`
💡 人間追記: 補足。今回扱ったコードの最終形
scripts/todo.py
"""
Gemini CLI用のタスク管理ユーティリティスクリプト。
GitブランチごとにMarkdown形式のTODOファイルを管理し、タスクの追加、表示、開始、完了をサポートします。
"""
import sys
import re
import os
import subprocess
from datetime import datetime
TASK_DIR = ".gemini/tasks"
def get_branch_name():
"""現在のGitブランチ名を取得し、ファイル名に使用可能な形式に変換します。"""
try:
return subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode().strip().replace("/", "-")
except:
return "default"
def get_todo_path():
"""現在のブランチに対応するTODOファイルのパスを返します。"""
return os.path.join(TASK_DIR, f"TODO-{get_branch_name()}.md")
def add(task):
"""新しいタスクをTODOファイルに追加します。"""
with open(get_todo_path(), "a") as f:
f.write(f"- [ ] {task}\n")
def init(title):
"""新しいTODOファイルを初期化します。"""
os.makedirs(TASK_DIR, exist_ok=True)
branch_name = get_branch_name()
path = get_todo_path()
with open(path, "w") as f:
f.write(f"""# TASK: {title}
- Branch: {branch_name}
- Created: {datetime.now().strftime('%Y-%m-%d')}
""")
def show():
"""現在のTODOファイルの内容を表示します。"""
path = get_todo_path()
if not os.path.exists(path):
print("No active TODO for this branch.", end='')
return
with open(path, "r") as f:
print(f"\n--- {os.path.basename(path)} ---", end='')
print(f.read().rstrip('\n'), end='')
def start(pattern):
"""
指定されたパターンに一致する最初の未完了タスクを開始状態 [/] にします。
既に実行中のタスクがある場合はエラー終了します。
"""
path = get_todo_path()
if not os.path.exists(path):
print(f"Error: {path} not found.", end='')
sys.exit(1)
return
with open(path, "r") as f: lines = f.readlines()
if any("[/]" in l for l in lines):
print("ERROR: 他のタスクが実行中です。先に完了させてください。", end='')
sys.exit(1)
found = False
new_lines = []
for l in lines:
if not found and "[ ]" in l and re.search(pattern, l):
l = l.replace("[ ]", "[/]")
found = True
new_lines.append(l)
if found:
with open(path, "w") as f:
f.writelines(new_lines)
print(f"Started: {pattern}", end='')
else:
print(f"Error: Task matching '{pattern}' not found or already started.", end='')
sys.exit(1)
def done():
"""現在実行中のタスク [/] を完了状態 [x] にします。"""
path = get_todo_path()
with open(path, "r") as f: lines = f.readlines()
new_lines = []
found = False
for l in lines:
if "[/]" in l:
l = l.replace("[/]", "[x]")
found = True
new_lines.append(l)
if found:
with open(path, "w") as f:
f.writelines(new_lines)
print("Task marked as DONE.", end='')
else:
print("No in-progress task found to mark as DONE.", end='')
def main():
if len(sys.argv) < 2:
print("Usage: todo.py [init|add|start|done|show] [args]", end='')
sys.exit(1)
return
cmd = sys.argv[1]
if cmd == "init":
if len(sys.argv) < 3:
print("Usage: todo.py init <title>", end='')
sys.exit(1)
return
init(sys.argv[2])
elif cmd == "add":
if len(sys.argv) < 3:
print("Usage: todo.py add <task>", end='')
sys.exit(1)
return
add(sys.argv[2])
elif cmd == "start":
if len(sys.argv) < 3:
print("Usage: todo.py start <pattern>", end='')
sys.exit(1)
return
start(sys.argv[2])
elif cmd == "done":
done()
elif cmd == "show":
show()
else:
print("Usage: todo.py [init|add|start|done|show] [args]", end='')
sys.exit(1)
return
if __name__ == "__main__":
main()
tests/test_todo.py
import os
import unittest
from unittest.mock import patch, mock_open, call
from datetime import datetime
import sys
from io import StringIO
import subprocess # subprocessをモックするためにインポート
from scripts import todo
class TestTodoScript(unittest.TestCase):
# テスト後に生成されたファイルをクリーンアップするためのセットアップ
def setUp(self):
self.test_dir = os.path.join(".gemini", "tasks")
# Ensure the directory exists before starting tests that might need it
os.makedirs(self.test_dir, exist_ok=True)
self.todo_file_path = os.path.join(self.test_dir, "TODO-test-feature-branch.md")
# テスト実行前にファイルが存在する場合は削除
if os.path.exists(self.todo_file_path):
os.remove(self.todo_file_path)
# 各テストメソッドの後に実行されるクリーンアップ
def tearDown(self):
if os.path.exists(self.todo_file_path):
os.remove(self.todo_file_path)
# .gemini/tasks ディレクトリが空になったら削除
# ただし、複数のテストが同じディレクトリを使う場合は注意が必要
# ここでは、このテストスイートで作成されたファイルのみを削除する
if os.path.exists(self.test_dir) and not os.listdir(self.test_dir):
os.rmdir(self.test_dir)
@patch('scripts.todo.get_branch_name')
def test_init_creates_file_with_header(self, mock_get_branch_name):
"""
`init` コマンドが正しいヘッダーを持つ新しいTODOファイルを作成することをテストします。
"""
# --- 前準備 ---
test_branch = "test-feature-branch"
test_title = "My New Task"
mock_get_branch_name.return_value = test_branch
expected_path = os.path.join(".gemini", "tasks", f"TODO-{test_branch}.md")
today_str = datetime.now().strftime('%Y-%m-%d')
# --- 実行 ---
todo.init(test_title)
# --- 検証 ---
# 1. ファイルが存在することを確認 (モックではなく実際のファイルシステムで)
self.assertTrue(os.path.exists(expected_path))
# 2. ファイルの内容を確認
with open(expected_path, "r") as f:
written_content = f.read()
# ヘッダーの各部分が存在することを確認
self.assertIn(f"# TASK: {test_title}", written_content)
self.assertIn(f"- Branch: {test_branch}", written_content)
self.assertIn(f"- Created: {today_str}", written_content)
@patch('scripts.todo.get_branch_name')
def test_add_appends_task_to_file(self, mock_get_branch_name):
"""
`add` コマンドが新しいタスクをTODOファイルに追記することをテストします。
"""
# --- 前準備 ---
test_branch = "test-feature-branch"
test_title = "Initial Task List"
first_task = "Buy groceries"
second_task = "Do laundry"
mock_get_branch_name.return_value = test_branch
# initでファイルを作成しておく
todo.init(test_title)
# --- 実行 ---
todo.add(first_task)
todo.add(second_task)
# --- 検証 ---
# ファイルの内容を確認
with open(self.todo_file_path, "r") as f:
content = f.read()
# タスクが正しい形式で追記されていることを確認
self.assertIn(f"- [ ] {first_task}\n", content)
self.assertIn(f"- [ ] {second_task}\n", content)
# タスクが既存のコンテンツの後に追記されていることを確認 (順序も含む)
init_header_end_index = content.find(f"- Created: {datetime.now().strftime('%Y-%m-%d')}")
first_task_index = content.find(f"- [ ] {first_task}")
second_task_index = content.find(f"- [ ] {second_task}")
self.assertGreater(first_task_index, init_header_end_index, "最初のタスクはヘッダーの後にあるべきです。")
self.assertGreater(second_task_index, first_task_index, "2番目のタスクは1番目のタスクの後にあるべきです。")
@patch('scripts.todo.get_branch_name')
@patch('sys.stdout', new_callable=StringIO) # sys.stdoutをモック
def test_show_displays_file_content(self, mock_stdout, mock_get_branch_name):
"""
`show` コマンドがTODOファイルの内容を標準出力に表示することをテストします。
"""
# --- 前準備 ---
test_branch = "test-feature-branch"
test_title = "Shopping List"
task1 = "Apples"
task2 = "Milk"
mock_get_branch_name.return_value = test_branch
# ファイルを初期化し、タスクを追加
todo.init(test_title)
todo.add(task1)
todo.add(task2)
# 期待される出力内容を構築 (ファイルの内容と一致するはず)
expected_output_prefix = f"\n--- {os.path.basename(self.todo_file_path)} ---"
with open(self.todo_file_path, "r") as f:
expected_file_content = f.read().rstrip('\n') # show関数のrstrip('\n')に合わせる
expected_full_output = expected_output_prefix + expected_file_content
# --- 実行 ---
todo.show()
# --- 検証 ---
# 標準出力の内容を確認
self.assertEqual(expected_full_output, mock_stdout.getvalue())
@patch('scripts.todo.get_branch_name')
@patch('sys.stdout', new_callable=StringIO)
def test_show_no_active_todo_message_if_file_not_exists(self, mock_stdout, mock_get_branch_name):
"""
TODOファイルが存在しない場合に `show` コマンドが特定のメッセージを表示することをテストします。
"""
# --- 前準備 ---
test_branch = "test-feature-branch"
mock_get_branch_name.return_value = test_branch
# ファイルが存在しない状態を確実にする (setUpで削除されているはず)
self.assertFalse(os.path.exists(self.todo_file_path))
# --- 実行 ---
todo.show()
# --- 検証 ---
# 標準出力の内容が期待されるメッセージであることを確認
self.assertEqual("No active TODO for this branch.", mock_stdout.getvalue())
@patch('scripts.todo.get_branch_name')
def test_start_marks_first_matching_task_as_in_progress(self, mock_get_branch_name):
"""
`start` コマンドが、最初に一致する未完了タスクを進行中 (`[/]`) に変更することをテストします。
"""
# --- 前準備 ---
test_branch = "test-feature-branch"
mock_get_branch_name.return_value = test_branch
todo.init("Tasks")
todo.add("Task 1")
todo.add("Task 2 [Important]")
todo.add("Task 3")
# --- 実行 ---
todo.start("Task 2")
# --- 検証 ---
with open(self.todo_file_path, "r") as f:
content = f.read()
self.assertIn("- [/] Task 2 [Important]\n", content)
self.assertIn("- [ ] Task 1\n", content) # 他のタスクは変更されない
self.assertIn("- [ ] Task 3\n", content)
@patch('scripts.todo.get_branch_name')
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.exit')
def test_start_exits_if_task_already_in_progress(self, mock_exit, mock_stdout, mock_get_branch_name):
"""
既に進行中のタスクがある場合に `start` コマンドがエラーを表示して終了することをテストします。
"""
# --- 前準備 ---
test_branch = "test-feature-branch"
mock_get_branch_name.return_value = test_branch
todo.init("Tasks")
todo.add("Task 1")
todo.add("Task 2")
todo.start("Task 1") # 最初のタスクを開始状態にする
# --- 実行 ---
todo.start("Task 2") # 別のタスクを開始しようとする
# --- 検証 ---
self.assertIn("ERROR: 他のタスクが実行中です。先に完了させてください。", mock_stdout.getvalue())
mock_exit.assert_called_once_with(1)
@patch('scripts.todo.get_branch_name')
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.exit')
def test_start_exits_if_no_matching_task_found(self, mock_exit, mock_stdout, mock_get_branch_name):
"""
一致する未完了タスクが見つからない場合に `start` コマンドがエラーを表示して終了することをテストします。
"""
# --- 前準備 ---
test_branch = "test-feature-branch"
mock_get_branch_name.return_value = test_branch
todo.init("Tasks")
todo.add("Task 1")
# --- 実行 ---
todo.start("NonExistent Task")
# --- 検証 ---
self.assertIn("Error: Task matching 'NonExistent Task' not found or already started.", mock_stdout.getvalue())
mock_exit.assert_called_once_with(1)
@patch('scripts.todo.get_branch_name')
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.exit')
def test_start_exits_if_file_not_exists(self, mock_exit, mock_stdout, mock_get_branch_name):
"""
タスクファイルが存在しない場合に `start` コマンドがエラーを表示して終了することをテストします。
"""
# --- 前準備 ---
test_branch = "test-feature-branch"
mock_get_branch_name.return_value = test_branch
# ファイルが存在しない状態を確実にする
self.assertFalse(os.path.exists(self.todo_file_path))
# --- 実行 ---
todo.start("Any Task")
# --- 検証 ---
self.assertIn(f"Error: {self.todo_file_path} not found.", mock_stdout.getvalue())
mock_exit.assert_called_once_with(1)
@patch('scripts.todo.get_branch_name')
def test_done_marks_in_progress_task_as_completed(self, mock_get_branch_name):
"""
`done` コマンドが現在進行中のタスク (`[/]`) を完了 (`[x]`) に変更することをテストします。
"""
# --- 前準備 ---
test_branch = "test-feature-branch"
mock_get_branch_name.return_value = test_branch
todo.init("Tasks")
todo.add("Task A")
todo.add("Task B")
todo.start("Task A") # Task Aを進行状態にする
# --- 実行 ---
todo.done()
# --- 検証 ---
with open(self.todo_file_path, "r") as f:
content = f.read()
self.assertIn("- [x] Task A\n", content)
self.assertIn("- [ ] Task B\n", content) # 他のタスクは変更されない
@patch('scripts.todo.get_branch_name')
@patch('sys.stdout', new_callable=StringIO)
def test_done_no_in_progress_task_message_if_none(self, mock_stdout, mock_get_branch_name):
"""
進行中のタスクがない場合に `done` コマンドが特定のメッセージを表示することをテストします。
"""
# --- 前準備 ---
test_branch = "test-feature-branch"
mock_get_branch_name.return_value = test_branch
todo.init("Tasks")
todo.add("Task A")
# --- 実行 ---
todo.done()
# --- 検証 ---
self.assertEqual("No in-progress task found to mark as DONE.", mock_stdout.getvalue())
@patch('sys.argv', ['todo.py'])
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.exit')
def test_main_exits_with_usage_if_no_args(self, mock_exit, mock_stdout):
"""
引数なしで `main` を呼び出した場合、使用方法が表示され `sys.exit(1)` が呼び出されることをテストします。
"""
# --- 実行 ---
todo.main()
# --- 検証 ---
self.assertIn("Usage: todo.py [init|add|start|done|show] [args]", mock_stdout.getvalue())
mock_exit.assert_called_once_with(1)
@patch('sys.argv', ['todo.py', 'init', 'My Title'])
@patch('scripts.todo.init')
def test_main_calls_init_with_args(self, mock_init):
"""
`main` が `init` コマンドと引数を適切に呼び出すことをテストします。
"""
# --- 実行 ---
todo.main()
# --- 検証 ---
mock_init.assert_called_once_with('My Title')
@patch('sys.argv', ['todo.py', 'add', 'New Task'])
@patch('scripts.todo.add')
def test_main_calls_add_with_args(self, mock_add):
"""
`main` が `add` コマンドと引数を適切に呼び出すことをテストします。
"""
# --- 実行 ---
todo.main()
# --- 検証 ---
mock_add.assert_called_once_with('New Task')
@patch('sys.argv', ['todo.py', 'start', 'Task 1'])
@patch('scripts.todo.start')
def test_main_calls_start_with_args(self, mock_start):
"""
`main` が `start` コマンドと引数を適切に呼び出すことをテストします。
"""
# --- 実行 ---
todo.main()
# --- 検証 ---
mock_start.assert_called_once_with('Task 1')
@patch('sys.argv', ['todo.py', 'done'])
@patch('scripts.todo.done')
def test_main_calls_done(self, mock_done):
"""
`main` が `done` コマンドを適切に呼び出すことをテストします。
"""
# --- 実行 ---
todo.main()
# --- 検証 ---
mock_done.assert_called_once()
@patch('sys.argv', ['todo.py', 'show'])
@patch('scripts.todo.show')
def test_main_calls_show(self, mock_show):
"""
`main` が `show` コマンドを適切に呼び出すことをテストします。
"""
# --- 実行 ---
todo.main()
# --- 検証 ---
mock_show.assert_called_once()
@patch('sys.argv', ['todo.py', 'invalid_command'])
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.exit')
def test_main_exits_with_usage_if_invalid_command(self, mock_exit, mock_stdout):
"""
不正なコマンドで `main` を呼び出した場合、使用方法が表示され `sys.exit(1)` が呼び出されることをテストします。
"""
# --- 実行 ---
todo.main()
# --- 検証 ---
self.assertIn("Usage: todo.py [init|add|start|done|show] [args]", mock_stdout.getvalue())
mock_exit.assert_called_once_with(1)
@patch('scripts.todo.subprocess.check_output')
def test_get_branch_name_returns_correct_name(self, mock_check_output):
"""
`get_branch_name` が実際の Git コマンドを実行し、正しいブランチ名を返すことをテストします。
"""
# --- 前準備 ---
mock_check_output.return_value = b'feature/my-new-feature\n'
# --- 実行 ---
branch_name = todo.get_branch_name()
# --- 検証 ---
mock_check_output.assert_called_once_with(["git", "rev-parse", "--abbrev-ref", "HEAD"])
self.assertEqual("feature-my-new-feature", branch_name)
@patch('scripts.todo.subprocess.check_output', side_effect=subprocess.CalledProcessError(1, "git"))
def test_get_branch_name_returns_default_on_error(self, mock_check_output):
"""
`get_branch_name` が Git コマンドでエラーが発生した場合に "default" を返すことをテストします。
"""
# --- 実行 ---
branch_name = todo.get_branch_name()
# --- 検証 ---
self.assertEqual("default", branch_name)
@patch('sys.argv', ['todo.py', 'init'])
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.exit')
def test_main_exits_with_usage_if_init_missing_args(self, mock_exit, mock_stdout):
"""
`init` コマンドで引数が不足している場合に `main` がエラーを表示して終了することをテストします。
"""
# --- 実行 ---
todo.main()
# --- 検証 ---
self.assertIn("Usage: todo.py init <title>", mock_stdout.getvalue())
mock_exit.assert_called_once_with(1)
@patch('sys.argv', ['todo.py', 'add'])
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.exit')
def test_main_exits_with_usage_if_add_missing_args(self, mock_exit, mock_stdout):
"""
`add` コマンドで引数が不足している場合に `main` がエラーを表示して終了することをテストします。
"""
# --- 実行 ---
todo.main()
# --- 検証 ---
self.assertIn("Usage: todo.py add <task>", mock_stdout.getvalue())
mock_exit.assert_called_once_with(1)
@patch('sys.argv', ['todo.py', 'start'])
@patch('sys.stdout', new_callable=StringIO)
@patch('sys.exit')
def test_main_exits_with_usage_if_start_missing_args(self, mock_exit, mock_stdout):
"""
`start` コマンドで引数が不足している場合に `main` がエラーを表示して終了することをテストします。
"""
# --- 実行 ---
todo.main()
# --- 検証 ---
self.assertIn("Usage: todo.py start <pattern>", mock_stdout.getvalue())
mock_exit.assert_called_once_with(1)
以上です。
