בעמוד הזה
בדיקת המערכת: Swagger UI, curl ותסריט בדיקה ב-Python
- לבדוק כל Endpoint ב-Swagger UI — Try it out, Body, Execute וקריאת התשובה
- לשלוח בקשות POST, PUT ו-DELETE עם curl
- לכתוב תסריט Python שמריץ סדרת בקשות ובודק שכל קוד סטטוס הוא הצפוי
- לבנות רשימת בדיקות ידנית (Test Plan) לכל פעולות ה-CRUD ומקרי הקצה
מה נלמד?
שרת שלא נבדק הוא שרת שלא ברור אם עובד. נלמד שלוש דרכים, מהידנית לאוטומטית:
| כלי | מתי |
|---|---|
Swagger UI (/docs) | בדיקה ידנית ונוחה, תוך כדי פיתוח |
| curl | בדיקה מהירה מה-Terminal, והרגל של אנשי Backend ו-DevOps |
| תסריט Python | סדרת בדיקות שרצה בלחיצה אחת, ובודקת הכול, בכל פעם |
ורשימת בדיקות (Test Plan): מה בודקים, ומה מצפים לקבל.
למה צריך את זה?
כל שינוי בקוד עלול לשבור משהו שעבד. אם הבדיקה היא “פתחתי את הדפדפן ונראה בסדר”, באגים יגיעו למשתמשים. רשימת בדיקות קבועה, ובמיוחד תסריט שמריץ אותה, מאפשרת לשנות קוד בביטחון. זה גם תרגול לקראת הפרויקט של נושא פרויקט האינטגרציה, שבו יום שלם מוקדש ל-Debugging ולבדיקות ידניות של כל פעולות ה-CRUD.
הסבר מהבסיס
Swagger UI
Swagger UI הוא דף ש-FastAPI בונה אוטומטית מתוך הקוד: מה-Routes, מה-Type Hints וממודלי Pydantic. הוא מציג כל Endpoint, ומאפשר לשלוח בקשות אמיתיות מהדפדפן.
http://127.0.0.1:8000/docsהתהליך לכל Endpoint:
- לחיצה על ה-Endpoint (למשל
POST /students) פותחת אותו. - Try it out ← השדות נפתחים לעריכה.
- ממלאים פרמטרים, ו/או עורכים את ה-Request body (JSON לדוגמה כבר מופיע, בנוי מהמודל!).
- Execute ← נשלחת בקשה אמיתית לשרת.
- מתחת: Curl (הפקודה המקבילה!), Request URL, Response body, Response code ו-Response headers.
בונוס: גם http://127.0.0.1:8000/openapi.json קיים. זה התיאור של ה-API בפורמט סטנדרטי (OpenAPI), שממנו Swagger בונה את הדף. כלים אחרים יודעים לקרוא אותו.
curl — תזכורת
curl -i http://127.0.0.1:8000/students
curl -i -X POST http://127.0.0.1:8000/students -H "Content-Type: application/json" -d '{"name": "Tom", "age": 27, "email": "tom@mail.com"}'
curl -i -X DELETE http://127.0.0.1:8000/students/3-i מציג גם את שורת הסטטוס, וזה בדיוק מה שבודקים.
ב-PowerShell, המירכאות בתוך JSON מסובכות. שתי דרכים נוחות: (1) לשמור את ה-JSON בקובץ (new_student.json) ולשלוח עם curl.exe -i -X POST URL -H "Content-Type: application/json" -d "@new_student.json". (2) להעתיק את פקודת ה-curl ש-Swagger מציג ולהריץ אותה ב-Git Bash.
תסריט בדיקה ב-Python
תסריט ששולח בקשות לשרת שרץ, ובודק לכל אחת: האם הקוד הוא מה שציפינו? בסוף הוא מדפיס כמה בדיקות עברו. בדיקה כזו, שמריצה את המערכת מבחוץ כמו Client אמיתי, נקראת בדיקת End-to-End (מקצה לקצה). (בהמשך המסלול תלמדו כלי בדיקות מקצועיים, כמו pytest. הרעיון זהה.)
Test Plan — רשימת בדיקות
לכל Endpoint: המסלול השמח + כל מקרה כישלון בטבלת המיפוי. אם לכל שורה בטבלה יש בדיקה, יודעים שהחוזה של ה-API מתקיים.
תחביר
import json
import urllib.error
import urllib.request
BASE_URL = "http://127.0.0.1:8000"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
request = urllib.request.Request(BASE_URL + path, data=data, method=method,
headers={"Content-Type": "application/json"})
try:
response = urllib.request.urlopen(request)
status, raw = response.status, response.read()
except urllib.error.HTTPError as error:
status, raw = error.code, error.read()
return status, (json.loads(raw) if raw else None)call מחזירה tuple של (קוד, גוף התשובה), גם בהצלחה וגם בשגיאה (HTTPError, נושא Backend ו-FastAPI). גוף ריק (204) → None.
דוגמת קוד ראשונה
Swagger — יצירת סטודנט צעד אחר צעד:
- הריצו את השרת (
uvicorn main:app --reload) ופתחו את/docs. - POST /students → Try it out. ב-Request body מופיע:
{
"name": "string",
"age": 0,
"email": "string"
}- החליפו ל:
{
"name": "Tom",
"age": 27,
"email": "tom@mail.com"
}- Execute. התוצאה:
Code: 201
Response body:
{
"id": 3,
"name": "Tom",
"age": 27,
"email": "tom@mail.com"
}- לחצו Execute שוב, עם אותו Body. עכשיו: 409,
"a student with email tom@mail.com already exists".
פירוק הקוד שורה אחר שורה
| מה רואים ב-Swagger | מאיפה זה מגיע בקוד |
|---|---|
| הרשימה של כל ה-Endpoints | ה-Decorators @app.get/post/put/delete |
| שם ה-API למעלה (“Students API”) | FastAPI(title="Students API") |
| ה-JSON לדוגמה ב-Body | מודל StudentIn (שדות וטיפוסים) |
השדה student_id עם integer | ה-Type Hint student_id: int |
| קוד 201 כ"Successful Response" ב-POST | status_code=201 |
| ה-id 3 בתשובה | db.create_student → AUTO_INCREMENT |
הכול נגזר מהקוד. כשמשנים Route, התיעוד מתעדכן לבד. לכן הוא תמיד מדויק.
דוגמה נוספת
תסריט בדיקה מקצה לקצה. smoke_test.py (השרת צריך לרוץ ב-Terminal אחר, על טבלה ריקה: ב-Database Client מריצים DROP TABLE students;, ומפעילים את השרת מחדש — init_db יוצרת את הטבלה מחדש):
import json
import urllib.error
import urllib.request
BASE_URL = "http://127.0.0.1:8000"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
request = urllib.request.Request(BASE_URL + path, data=data, method=method,
headers={"Content-Type": "application/json"})
try:
response = urllib.request.urlopen(request)
status, raw = response.status, response.read()
except urllib.error.HTTPError as error:
status, raw = error.code, error.read()
return status, (json.loads(raw) if raw else None)
CHECKS = [
("create Dana", "POST", "/students", {"name": "Dana", "age": 24, "email": "dana@mail.com"}, 201),
("create Avi", "POST", "/students", {"name": "Avi", "age": 31, "email": "avi@mail.com"}, 201),
("duplicate email", "POST", "/students", {"name": "Dana Copy", "age": 20, "email": "dana@mail.com"}, 409),
("too young", "POST", "/students", {"name": "Kid", "age": 7, "email": "kid@mail.com"}, 400),
("missing fields", "POST", "/students", {"name": "Noa"}, 422),
("list", "GET", "/students", None, 200),
("get existing", "GET", "/students/1", None, 200),
("get missing", "GET", "/students/99", None, 404),
("bad id", "GET", "/students/abc", None, 422),
("update", "PUT", "/students/2", {"name": "Avi Levi", "age": 32, "email": "avi@mail.com"}, 200),
("update missing", "PUT", "/students/99", {"name": "Ghost", "age": 30, "email": "g@mail.com"}, 404),
("update clash", "PUT", "/students/2", {"name": "Avi", "age": 32, "email": "dana@mail.com"}, 409),
("delete", "DELETE", "/students/1", None, 204),
("delete again", "DELETE", "/students/1", None, 404),
]
def main():
passed = 0
for name, method, path, body, expected in CHECKS:
status, data = call(method, path, body)
ok = status == expected
passed = passed + (1 if ok else 0)
mark = "PASS" if ok else "FAIL"
print(mark, "|", name, "|", method, path, "| expected", expected, "got", status)
print(passed, "/", len(CHECKS), "checks passed")
if __name__ == "__main__":
main()פלט:
PASS | create Dana | POST /students | expected 201 got 201
PASS | create Avi | POST /students | expected 201 got 201
PASS | duplicate email | POST /students | expected 409 got 409
PASS | too young | POST /students | expected 400 got 400
PASS | missing fields | POST /students | expected 422 got 422
PASS | list | GET /students | expected 200 got 200
PASS | get existing | GET /students/1 | expected 200 got 200
PASS | get missing | GET /students/99 | expected 404 got 404
PASS | bad id | GET /students/abc | expected 422 got 422
PASS | update | PUT /students/2 | expected 200 got 200
PASS | update missing | PUT /students/99 | expected 404 got 404
PASS | update clash | PUT /students/2 | expected 409 got 409
PASS | delete | DELETE /students/1 | expected 204 got 204
PASS | delete again | DELETE /students/1 | expected 404 got 404
14 / 14 checks passedפירוק:
CHECKS— רשימה של tuples: שם הבדיקה, Method, Path, Body וקוד צפוי. הנתונים מופרדים מהלוגיקה (נושא Python!). הוספת בדיקה = הוספת שורה.- הסדר חשוב: הבדיקות תלויות זו בזו (קודם יוצרים, ואחר כך מוחקים). לכן מתחילים ממסד ריק.
- הלולאה — שולחת, משווה ל-
expected, ומדפיסה PASS/FAIL. - הקסם: משנים משהו בקוד (Refactoring, נושא Python–3), מריצים, ותוך שנייה רואים אם משהו נשבר. רשת ביטחון.
נסו לשבור: שנו ב-main.py את status_code=204 ל-200 ב-DELETE, ושמרו (--reload). מחקו את הטבלה (DROP TABLE students; ב-Database Client), הפעילו מחדש, והריצו את התסריט:
FAIL | delete | DELETE /students/1 | expected 204 got 200
13 / 14 checks passedהתסריט תפס את השינוי. החזירו את הקוד.
טעות נפוצה
הטעות 1: לבדוק רק את המסלול השמח. “יצרתי סטודנט, וזה עבד” ≠ “השרת עובד”. רוב הבאגים בקוד שרתים נמצאים במקרי הכישלון: 404 שחוזר כ-200, 409 שחוזר כ-500. Test Plan = מסלול שמח + כל שורת כישלון בטבלת המיפוי.
הטעות 2: להריץ את תסריט הבדיקה על מסד עם נתונים קודמים. “create Dana” ייכשל עם 409, כי דנה כבר קיימת מהריצה הקודמת. מסד ריק לפני כל הרצה (או, בהמשך המסלול, מסד בדיקות נפרד).
הטעות 3: לשכוח שהשרת צריך לרוץ.
urllib.error.URLError: <urlopen error [WinError 10061] No connection could be made because the target machine actively refused it>“Connection refused” = אין שרת שמאזין על הפורט (נושא מחשב, מערכת הפעלה ורשת!). מפעילים את uvicorn ב-Terminal נפרד.
הטעות 4: לבדוק רק את הקוד, ולא את הגוף. קוד 200 עם גוף שגוי (שם לא מעודכן) הוא עדיין באג. בבדיקות חשובות, בודקים גם ערכים: data["name"] == "Avi Levi". (ראו תרגול עצמאי.)
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: Test Plan
כתבו Test Plan ל-Students API: לפחות 8 בדיקות, עם ה-Method, הנתיב, מה שולחים, והקוד שמצפים לקבל. כללו את המסלול השמח וגם מקרי כישלון.
הצג פתרון
| # | בדיקה | בקשה | קוד צפוי |
|---|---|---|---|
| 1 | יצירת דנה | POST /students עם גוף תקין | 201 |
| 2 | אימייל כפול | POST /students עם האימייל של דנה | 409 |
| 3 | גיל צעיר מדי | POST /students עם גיל 7 | 400 |
| 4 | שדות חסרים | POST /students עם שם בלבד | 422 |
| 5 | סטודנט קיים | GET /students/1 | 200 |
| 6 | סטודנט שלא קיים | GET /students/99 | 404 |
| 7 | id לא מספרי | GET /students/abc | 422 |
| 8 | מחיקה | DELETE /students/1 | 204 |
| 9 | מחיקה שוב | DELETE /students/1 | 404 |
הבדיקה הראשונה תמיד עוברת — רוב הבאגים במקרי הכישלון: 404 שחוזר כ-200, או 409 שחוזר כ-500.
תרגיל 2: curl
כתבו פקודות curl לשרת שרץ על http://127.0.0.1:8000:
- יצירת סטודנט (דנה, 24, dana@mail.com) — עם הצגת הקוד.
- קריאת סטודנט 1.
- מחיקת סטודנט 1 — עם הצגת הקוד.
הצג פתרון
curl -i -X POST http://127.0.0.1:8000/students -H "Content-Type: application/json" -d '{"name": "Dana", "age": 24, "email": "dana@mail.com"}'
curl http://127.0.0.1:8000/students/1
curl -i -X DELETE http://127.0.0.1:8000/students/1-i מציג את ה-Status Code — בבדיקות הוא הדבר הכי חשוב לראות. ב-Swagger (/docs) אפשר לבצע את אותן בקשות מהדפדפן.
תרגיל 3: הפונקציה call
כתבו פונקציה call(method, path, body=None) ששולחת בקשה לשרת עם urllib.request, ומחזירה tuple של (קוד, גוף כמילון או None). שגיאות HTTP (4xx, 5xx) לא צריכות לקרוס — גם הן מחזירות קוד.
הצג פתרון
import json
import urllib.error
import urllib.request
BASE_URL = "http://127.0.0.1:8000"
def call(method, path, body=None):
data = None
if body is not None:
data = json.dumps(body).encode()
request = urllib.request.Request(BASE_URL + path, data=data, method=method,
headers={"Content-Type": "application/json"})
try:
response = urllib.request.urlopen(request)
status, raw = response.status, response.read()
except urllib.error.HTTPError as error:
status, raw = error.code, error.read()
if raw:
return status, json.loads(raw)
return status, NoneurlopenזורקתHTTPErrorעל 4xx ו-5xx — ובבדיקות אלה בדיוק התשובות שמעניינות אותנו, ולכן תופסים אותן ומחזירים את הקוד.- תשובה של 204 היא בלי גוף — ולכן
None.
תרגיל 4: התסריט שמדפיס PASS / FAIL
כתבו את החלק שמריץ את רשימת הבדיקות ומדפיס תוצאה. כדי לתרגל בלי שרת, התוצאות כבר נמצאות ברשימה: שם הבדיקה, הקוד הצפוי והקוד שהתקבל.
results = [
("create Dana", 201, 201),
("duplicate email", 409, 409),
("get missing", 404, 200),
("delete", 204, 204),
]פלט צפוי:
PASS | create Dana | expected 201 got 201
PASS | duplicate email | expected 409 got 409
FAIL | get missing | expected 404 got 200
PASS | delete | expected 204 got 204
3 / 4 checks passedהצג פתרון
results = [
("create Dana", 201, 201),
("duplicate email", 409, 409),
("get missing", 404, 200),
("delete", 204, 204),
]
passed = 0
for name, expected, got in results:
if expected == got:
status = "PASS"
passed = passed + 1
else:
status = "FAIL"
print(status, "|", name, "| expected", expected, "got", got)
print(passed, "/", len(results), "checks passed")PASS | create Dana | expected 201 got 201
PASS | duplicate email | expected 409 got 409
FAIL | get missing | expected 404 got 200
PASS | delete | expected 204 got 204
3 / 4 checks passedעם שרת אמיתי, got מגיע מ-call(...). ה-FAIL כאן חושף באג אמיתי: סטודנט שלא קיים חוזר כ-200 במקום 404.
תרגיל 5: השרת לא רץ
מריצים את תסריט הבדיקה, ומקבלים:
urllib.error.URLError: <urlopen error [WinError 10061] No connection could be made
because the target machine actively refused it>- מה המשמעות?
- איך מריצים נכון את הבדיקות?
- למה כדאי להתחיל ממסד ריק?
הצג פתרון
- אין שרת שמאזין בכתובת ובפורט — החיבור נדחה. התסריט הוא Client, והוא צריך שרת שרץ.
- שני Terminals: באחד מריצים
uvicorn main:app, ובשני מריצים את תסריט הבדיקות. - כדי שהתוצאות יהיו צפויות: ה-id של הסטודנט הראשון הוא 1, והרשימה מתחילה ריקה. על מסד עם נתונים ישנים, “יצירת דנה” עלולה להחזיר 409 כי היא כבר קיימת — והבדיקה תיכשל בלי שיש באג.
סיכום
- Swagger UI (
/docs): Try it out → Body → Execute. נבנה אוטומטית מהקוד, ומציג גם את פקודת ה-curl המקבילה. - curl -i: בדיקה מהירה מה-Terminal. ב-PowerShell, JSON מקובץ (
-d "@file.json") או Git Bash. - תסריט Python (End-to-End): רשימת בדיקות (נתונים) + לולאה (לוגיקה) ← PASS/FAIL. רשת ביטחון לכל שינוי.
- Test Plan = מסלול שמח + כל מקרה כישלון בטבלת המיפוי. בודקים קודים וגם גוף.
- מריצים בדיקות על מסד ריק, ועם שרת פעיל.
דוקומנטציה רשמית
- FastAPI — Interactive API docs — Swagger UI ב-/docs
- curl — manual — בקשות מה-Terminal
- urllib.request — בקשות מתוך Python