האם Test Driven Development עדיין רלוונטי?
פיתוח מונחה בדיקות או TDD הוא הרעיון שכדאי לי לכתוב קודם את הבדיקות ואז לראות את הקוד, ואז אני מרוויח גם בדיקות של הקוד וגם קוד טוב יותר שיותר קל לבדוק אותו. ניסיתי את זה היום עם AI וזה עבד יופי, כלומר כתבתי כמה בדיקות שמשתמשות בפונקציות ומחלקות שהמצאתי, נתתי ל AI לכתוב את הקוד, לפעמים זה הצליח לו, לפעמים הוא התלונן שהבדיקות שלי לא הגיוניות. כשהוא הצליח הכל היה טוב הכנסתי את הקוד לגיט והתקדמתי. כשהוא התלונן ניקיתי את מה שהוא כתב, תיקנתי את הבדיקות ושלחתי אותו לייצר שוב.
בסוף קיבלתי קוד עובד שעשה בדיוק את מה שרציתי ובמבנה מחלקות שאני בחרתי. לפני שנתיים הייתי חותם על זה. חלום. היום אני לא כל כך בטוח.
האלטרנטיבה של TDD בה גם אני משתמש וגם אני רואה בתעשייה היא PDD, כלומר Prompt Driven Development. כותבים פרומפט, נותנים ל AI לייצר קוד ובדיקות, קוראים את הבדיקות והקוד, אם אוהבים שומרים ואם לא אוהבים מנקים ומתקנים את הפרומפט.
לדוגמה עבור משחק איקס עיגול ב TDD אני מתחיל עם קובץ בדיקות כזה:
import pytest
import GameUI
def test_first_player_plays(game_logic):
game_logic.play(0, 0)
assert not game_logic.game_over()
assert game_logic.winner() is None
def test_second_player_plays_in_taken_square(game_logic):
game_logic.play(0, 0)
with pytest.raises(Exception):
game_logic.play(0, 0)
def test_player1_wins_first_row(game_logic):
game_logic.play(0, 0)
game_logic.play(1, 0)
game_logic.play(0, 1)
game_logic.play(1, 1)
game_logic.play(0, 2)
assert game_logic.game_over()
assert game_logic.winner() == game_logic.players[0]
def test_player2_wins_diagonal(game_logic):
game_logic.play(0, 1)
game_logic.play(0, 0)
game_logic.play(1, 0)
game_logic.play(1, 1)
game_logic.play(1, 2)
game_logic.play(2, 2)
assert game_logic.game_over()
assert game_logic.winner() == game_logic.players[1]
def test_cant_play_after_win(game_logic):
game_logic.play(0, 1)
game_logic.play(0, 0)
game_logic.play(1, 0)
game_logic.play(1, 1)
game_logic.play(1, 2)
game_logic.play(2, 2)
with pytest.raises(Exception):
game_logic.play(2, 0)
def test_show_empty_board(game_logic, capsys):
GameUI.print_current_game(game_logic)
printed_text = capsys.readouterr().out
assert printed_text == """Waiting for player X
. . .
. . .
. . .
"""
def test_show_board_after_play(game_logic, capsys):
game_logic.play(0, 0)
game_logic.play(1, 1)
game_logic.play(2, 0)
GameUI.print_current_game(game_logic)
printed_text = capsys.readouterr().out
assert printed_text == """Waiting for player O
X . .
. O .
X . .
"""
def test_show_draw(game_logic, capsys):
game_logic.play(0, 0) # X
game_logic.play(1, 1) # O
game_logic.play(2, 0) # X
game_logic.play(1, 0) # O
game_logic.play(1, 2) # X
game_logic.play(2, 1) # O
game_logic.play(0, 1) # X
game_logic.play(0, 2) # O
game_logic.play(2, 2) # X
GameUI.print_current_game(game_logic)
printed_text = capsys.readouterr().out
assert printed_text == """Game Over. It's a draw
X X O
O O X
X O X
"""
def test_show_player_1_won(game_logic, capsys):
game_logic.play(0, 0)
game_logic.play(1, 0)
game_logic.play(0, 1)
game_logic.play(1, 1)
game_logic.play(0, 2)
GameUI.print_current_game(game_logic)
printed_text = capsys.readouterr().out
assert printed_text == """Bravo! X Won
X X X
O O .
. . .
"""
def test_read_move_from_player(game_logic, monkeypatch, capsys):
monkeypatch.setattr("builtins.input", lambda _: "0, 0")
next_move = GameUI.read_next_move(game_logic)
game_logic.play(*next_move)
capsys.readouterr() # clear the buffer
GameUI.print_current_game(game_logic)
printed_after = capsys.readouterr().out
assert printed_after == """Waiting for player O
X . .
. . .
. . .
"""
ואז בפרומפט מאוד פשוט מקבל את GameLogic שבדיוק מתאים לממשק שהגדרתי.
ב PDD אני פשוט כותב "תבנה לי משחק איקס עיגול עם בדיקות בפייתון" ומתקן בלולאה את הפרומפט עד שיוצא משחק שאני רוצה. צריך בדיקה ספציפית? אוסיף אותה לפרומפט. צריך מחלקה ספציפית? נגדיר אותה בפרומפט.
ההבדלים המרכזיים בין הגישות:
ב TDD אני לוקח שליטה על הקוד. אני יודע מה אני כותב ומה צריך להיכתב. אני קובע את המבנה של הקוד ויודע בדיוק איך יראה הקוד שה AI יבנה. כמעט לא הולך לי זמן על לחכות לסוכן הקידוד או לקרוא את הקוד שלו.
ב PDD אני מוותר על השליטה ובוחר מתפריט, בסגנון "תראה לי מה אפשר". פה הרבה יותר זמן הולך על להסתכל על סוכן הקידוד עובד ולקרוא את הקוד שלו. לפעמים אפשר ללמוד מהקוד שהוא יצר ולגלות דברים חדשים. לפעמים לא. בהיבט של בדיקות בגישת PDD יהיו לי הרבה יותר בדיקות אבל איכות חבילת הבדיקות הכללית תהיה פחות טובה. בדוגמה של האיקס עיגול כשנתתי ל AI לבנות את הקוד הוא באמת כתב לבד את הבדיקות ללוגיקה אבל לא מימש בדיקות לכתיבה למסך.
אז עשיתי ניסוי, נתתי לסוכן הקידוד את הפרומפט הבא:
create a text based tic tac toe game in python with pytest. use uv
Use multiple modules for game logic and display
Implement pytest tests for both logic and display
מאוד רציתי שהוא יכתוב בדיקות תצוגה פשוטות כמו שאני כתבתי בדוגמה שהדבקתי, אבל זה לא מה שקרה. מודול התצוגה שסוכן הקידוד יצר כולל פונקציה שמציירת את הלוח למערך של שורות ופונקציה אחרת שמדפיסה את המערך. הבדיקה היא עדיין בדיקת data שבודקת רק את מערך השורות כלומר:
def test_render_board_with_markers(self):
board = Board()
board.place_marker("A1", "X")
board.place_marker("B2", "O")
rendered = Display.render_board(board)
assert "X" in rendered
assert "O" in rendered
נראה ש glm עבד מאוד קשה רק בשביל לא להשתמש ב capsys של pytest והפך את קוד המשחק ליותר מסורבל.
אז בחזרה לשאלה שבכותרת, האם TDD עדיין רלוונטי? לפעמים. בעזרת TDD אני יכול להעביר את המסר לסוכן הקידוד בצורה הרבה יותר מדויקת מאשר בעזרת פרומפט ואני חוסך את הוויכוחים עם הסוכן, במיוחד כשאני יודע איזה קוד אני מצפה לראות.