Testing Guide
- summary:
Quickstart להרצת טסטים, ההנחיות הקריטיות, טעינת ה-stubs לטלגרם, עבודה עם tmp_path ומתכון מחיקה מוגבל ל-allowlist, ו-mocking של HTTP.
🚀 Quickstart לטסטים
הגדרת משתני סביבה (בזמן הרצה):
export DISABLE_ACTIVITY_REPORTER=1
export DISABLE_DB=1
export BOT_TOKEN=x
export MONGODB_URL='mongodb://localhost:27017/test'
התקנת תלויות טסטים וכיסוי:
pip install -U pytest pytest-asyncio pytest-cov
הרצות שימושיות:
# כל הטסטים במצב שקט
pytest -q
# בדיקת קובץ/טסט ספציפי
pytest tests/test_bot_handlers_show_command_more.py::test_show_command_renders_html_and_escapes_code_and_buttons_id -q
הנחיות קריטיות
כל IO בטסטים יתבצע תחת
tmp_pathבלבד.מחיקות יתבצעו רק תחת
/tmpבאמצעות wrapper בטוח.מבודדים את תלות
python-telegram-botבאמצעות Stubs כדי להימנע מקריאות אמתיות.
טעינת Stubs לטלגרם
כדי להריץ טסטים ללא python-telegram-bot, קיימים stubs ב-tests/_telegram_stubs.py והם נטענים אוטומטית דרך tests/conftest.py:
# tests/conftest.py
import os
os.environ.setdefault('DISABLE_ACTIVITY_REPORTER', '1')
os.environ.setdefault('DISABLE_DB', '1')
os.environ.setdefault('BOT_TOKEN', 'x')
os.environ.setdefault('MONGODB_URL', 'mongodb://localhost:27017/test')
import tests._telegram_stubs # noqa
דוגמת שימוש ב‑tmp_path
def test_file_operations(tmp_path):
test_file = tmp_path / "test.py"
test_file.write_text("print('hello')")
assert test_file.exists()
מחיקה בטוחה
from pathlib import Path
import shutil
def safe_rmtree(path: Path, allow_under: Path) -> None:
p = path.resolve()
base = allow_under.resolve()
if not (p == base or base in p.parents) or p in (Path('/'), base.parent, Path.cwd()):
raise RuntimeError(f"Refusing to delete unsafe path: {p}")
shutil.rmtree(p)
רישום Blueprint בסביבת טסטים
במהלך הרצת בדיקות (pytest), האפליקציה מבטיחה שרישום ה‑Blueprint של collections_api יבוצע תמיד — גם אם הייבוא נכשל או אם הקובץ config חסר.
מה קורה בפועל:
אם המודול נטען בהצלחה: ה‑Blueprint נרשם כרגיל תחת
/api/collectionsבאמצעותcollections_bp(אוbp).אם הייבוא נכשל או אין
bp: נרשם Blueprint דיאגנוסטי שמונע שגיאות 404 ומחזיר JSON עם סטטוס 503, למשל:{"ok": false, "error": "collections_api_unavailable", "diagnostic": true}
בפרודקשן: ההתנהגות לא משתנה — חריגים נרשמים ללוג בלבד, ואין Blueprint דמה.
דוגמה לקוד שמבטיח רישום בסביבת pytest (חלק מ‑webapp/app.py):
import os, sys
_is_pytest = (
bool(os.getenv("PYTEST_CURRENT_TEST"))
or ("pytest" in sys.modules)
or os.getenv("PYTEST") == "1"
or os.getenv("PYTEST_RUNNING") == "1"
)
if _is_pytest:
enabled = True # הפיצ'ר נכפה ל-True בזמן טסטים
בדיקות מול מונגו אמיתי
רוב הבדיקות בריפו רצות מול stub בפייתון טהור, וזה נכון: הן מהירות, אין להן תלות חיצונית, והן מכסות ניתוב, ולידציה, סדר פעולות וקודי שגיאה.
אבל יש דברים שסטאב לא יכול לבדוק — לא כי הוא חלש, אלא כי הם אינם בקוד אלא בהתנהגות של המסד:
אילוץ ייחודיות — אינדקס ייחודי-חלקי כמו
one_default_per_userהוא מה שסוגר מרוץ בין שתי בקשות מקבילות. רק המסד יכול לדחות, ו-create_indexשהחזיר בלי לזרוק אינו ראיה שהאינדקס נוצר.האם אינדקס בשימוש — רק
explainעונה. סטאב מחזיר תוצאה נכונה גם כששאילתה סורקת את כל האוסף.סמנטיקה של BSON —
datetimeנקטם למילישניות, ובליtz_aware=Trueהוא חוזר נאיבי. השוואה בין נאיבי ל-aware זורקתTypeError, ואם היא עטופה ב-except— הבדיקה שנשענת עליה מפסיקה לרוץ בשקט.צינורות aggregation — סטאב שנכתב ביד מבין רק את הצורה שנכתבה בו, ולכן שגיאת תחביר אמיתית עוברת אצלו.
בייטים מול תווים —
$substrBytesו-$strLenBytesמודדים בבייטים,$substrCPו-$strLenCPבתווים, ו-$regexFindמחזירidxבתווים. על טקסט עברי ערבוב היחידות חותך באמצע אות ומונגו זורקת. אף סטאב בריפו אינו מדגמן את זה —tests/_fake_mongo.pyאפילו אין בוaggregate.
הבדיקות האלה חיות בקבצים נפרדים, וכל אחד מהם נשען על משתנה סביבה משלו:
tests/test_note_boards_mongo.py—MONGODB_URL, דרךpytestmarkשנבדק פעם אחת בטעינת המודול.tests/test_profiler_projection_mongo.py—MONGODB_URL, גם הוא דרךpytestmark, ובנוסף שער גרסה בפיקסצ’ר (ראו למטה).tests/test_snippet_hebrew_offsets_mongo.py—NOTE_FONTS_TEST_MONGO_URI, דרך הפיקסצ’רwired_mongoשב-tests/conftest.py. אותו פיקסצ’ר משרת גם את שאר הבדיקות שמריצות את הראוטים של הוובאפ מול מסד אמיתי.tests/test_recycle_bin_ttl_index_mongo.py—NOTE_FONTS_TEST_MONGO_URI, ואם הוא ריקMONGODB_URL. ה-pytestmarkבודק רק שיש כתובת, והנגישות נבדקת פעם אחת בפיקסצ’רmongo_client, שמדלג רק עלServerSelectionTimeoutError(גם מארח שאינו נפתר נופל לשם) — אימות שגוי אוmongodb+srv://שאינו נפתר נכשלים ואינם מדלגים. בודק מה השרת עושה עם הבקשה ליצור את אינדקס ה-TTL של סל המיחזור. את המחיקה עצמה הוא אינו בודק: תהליך ה-TTL של השרת רץ פעם בדקה (TTL Indexes), ובדיקה שממתינה לו הייתה נוגעת בתקרת ה-timeoutשב-pytest.ini.
המשתנה הנפרד אינו כפילות מיותרת. tests/conftest.py עושה os.environ.setdefault('MONGODB_URL', …) בטעינה, כלומר המשתנה הזה תמיד מוגדר בבדיקות — לערך דמה. פיקסצ’ר שהיה נופל אליו היה מחכה 30 שניות לכתובת שאין מאחוריה שרת, בכל בדיקה, ואז נכשל — ו---maxfail=1 היה עוצר את כל החבילה.
כולם מדלגים כשהמשתנה שלהם ריק או כשהשרת אינו נגיש, כך שהרצה מקומית רגילה נשארת מהירה. שרת שכן נגיש אבל בגרסה נמוכה מדי הוא מקרה אחר לגמרי — שם הבדיקה נכשלת ואינה מדלגת, ראו את הפסקה על גרסת השרת למטה.
אזהרה
ב-CI של ה-PR הן אינן רצות. הג’וב Unit Tests ב-.github/workflows/ci.yml אמנם מרים mongo:8.0 כשירות, אבל הוא runs-on: ubuntu-latest בלי container:, והשירות מוגדר בלי ports:. לפי תיעוד GitHub Actions, גישה לפי שם השירות עובדת רק כשהג’וב עצמו רץ בקונטיינר; אחרת צריך למפות פורטים ולפנות ל-127.0.0.1:<port>. בלי זה המארח mongodb אינו נפתר כלל ([Errno -3] Temporary failure in name resolution), והבדיקות מדלגות בשקט.
התיקון הוא ports: על השירות ומעבר ל-127.0.0.1 — בדיוק כפי שהג’וב alembic-migrations באותו קובץ כבר עושה עבור postgres. הוא מוצא לסבב נפרד, כי הוא יעיר את הבדיקות האלה ואי אפשר לדעת מראש אילו מהן עוברות.
אחרי מיזוג הן כן רצות, וזה לא אותו קובץ. .github/workflows/deploy.yml מריץ את אותה חבילה על push ל-main, ושם השירות mongodb כן מוגדר עם ports: וה-MONGODB_URL מצביע ל-localhost:27017 — כלומר שרת נגיש, והבדיקות שנשענות על MONGODB_URL רצות במלואן. זה ההסבר לכשל שמופיע ”רק אחרי מיזוג“: אותה חבילה בדיוק, פעם אחת בלי מסד ופעם אחת איתו.
גרסת השרת: 8.0 ומעלה, ולא ”מונגו כלשהו“. הפרודקשן רץ על MongoDB Atlas 8.0, ולכן כל סביבות הבדיקה מרימות mongo:8.0 — .github/workflows/ci.yml, .github/workflows/deploy.yml, docker-compose.yml ו-docker-compose.dev.yml. בדיקה שרצה מול מסד אמיתי ובודקת מנוע אחר מזה שבפרודקשן היא ביטחון שווא, ולא כיסוי. מי שמרים את ה-compose מקומית על volume שנוצר בזמן של 6.0 צריך מעבר חד-פעמי לפני ההרצה הראשונה — ראו התקנה והגדרה.
ל-tests/test_profiler_projection_mongo.py זה קריטי במיוחד: הוא משווה את queryShapeHash שמונגו מחזירה ב-explain, ולפי התיעוד של explain, השדה הזה נוסף ב-MongoDB 8.0. מול 6.0 הוא פשוט אינו חוזר, וההשוואה מתרוקנת מתוכן. הפיקסצ’ר שם קורא buildInfo, ואם הגרסה נמוכה מ-8.0 הוא נכשל בהודעה שאומרת מה גרסת השרת ומה נדרש — ולא מדלג, כי דילוג היה צובע את הריצה בירוק בזמן שההשוואה היחידה שמוכיחה את התיקון אינה מתבצעת.
להרצה מקומית מול שרת אמיתי:
MONGODB_URL='mongodb://127.0.0.1:27017' pytest tests/test_note_boards_mongo.py -v
MONGODB_URL='mongodb://127.0.0.1:27017' pytest tests/test_profiler_projection_mongo.py -v
NOTE_FONTS_TEST_MONGO_URI='mongodb://127.0.0.1:27017' \
pytest tests/test_snippet_hebrew_offsets_mongo.py -v
NOTE_FONTS_TEST_MONGO_URI='mongodb://127.0.0.1:27017' \
pytest tests/test_recycle_bin_ttl_index_mongo.py -v
אזהרה
כל הקבצים האלה יוצרים מסד ייעודי משלהם ואינם נוגעים במסד ברירת המחדל: test_note_boards_mongo.py מגריל שם עם התחילית codebot_notes_it_, test_profiler_projection_mongo.py עם התחילית codebot_profiler_it_, test_recycle_bin_ttl_index_mongo.py עם התחילית codebot_recycle_ttl_it_, ו-wired_mongo בונה cktest_<שם קובץ הבדיקה>. ה-teardown של כל קובץ שמגריל שם מוודא שהשם תואם לתחילית לפני drop_database. עם זאת — אל תכוונו את אף אחד משני המשתנים למסד שיש בו נתונים אמיתיים.
כיסוי בדיקות (pytest-cov)
הפרויקט מגדיר
pytest-covב-pytest.ini. אם חסר, התקינו:pip install pytest-cov.דוחות:
pytest --cov=. --cov-report=term-missing --cov-report=xml
CI נתמך
ה‑PR חייב לעבור סטטוסים: ”🔍 Code Quality & Security“, ”Unit Tests (3.11)“, ”Unit Tests (3.12)“.
בדיקות דפדפן, ותקרת הזמן
בדיקות הדפדפן מדולגות כשאין דפדפן, וההכרעה נעשית פעם אחת לכל הריצה. הפיקסצ’ר chromium_executable ב-tests/conftest.py מנסה להרים Chromium פעם אחת, ואם אין — כל בדיקות הדפדפן מדולגות שם, לפני שאף אחת מהן מרימה תהליך דרייבר של Playwright בעצמה. ל-CI אין שלב שמתקין דפדפן, ולכן זה המצב הרגיל שם ולא תקלה — והריצה אומרת את זה במפורש בסיכום, כדי שלא ייראה כאילו הבדיקות רצו.
מכאן נובע שהכיסוי של בדיקות הדפדפן ב-CI הוא אפס. מי שרוצה להריץ אותן מקומית צריך דפדפן מותקן ואת PLAYWRIGHT_BROWSERS_PATH מכוון אליו; בלי זה הן מדולגות בשקט מוצהר.
ובדיקה שחורגת מהתקרה נכשלת בשמה, ואינה הורגת את התהליך. pytest.ini קובע timeout = 60 ואינו קובע timeout_method — ההשמטה מכוונת, ו-pytest-timeout בוחר בעצמו לפי הפלטפורמה. בשיטה thread, שהייתה כתובה שם קודם, התוסף קורא os._exit: הבדיקה אינה נכשלת אלא הורגת את התהליך שמריץ אותה.
זה מסביר את ההודעה node down: Not properly terminated שמופיעה בלוגים של ריצות עבר תחת pytest-xdist. היא באה מה-controller, שרואה תהליך שנעלם, ואינה נוקבת בשם הבדיקה — כי ה-worker כותב את המחסנית ל-stderr שלו ויוצא לפני שהפלט נשטף. הסימן שמזהה אותה בלוג הוא הפער: היא מופיעה בדיוק timeout שניות אחרי השורה האחרונה של אותו worker.
בדיקות ביצועים (Performance)
מרקרים:
[pytest] markers = performance: בדיקות ביצועים heavy: טסטים כבדים (מדולגים כשמבקשים רק קלים)
הרצות מקומיות:
# הכל pytest -q -m performance # רק קלים ONLY_LIGHT_PERF=1 pytest -q -m performance
CI: - ברירת מחדל מריץ הכל. - PR Draft + תווית
perf-lightמריץ רק קלים. - זמני ריצה נשמרים כארטיפקטים:durations.json,durations-summary.json.דוחות/מדידות:
pytest -m performance --durations=0 --json-report --json-report-file=durations.json cat durations.json | jq '.summary.durations' > durations-summary.json
קישורים
העמוד הזה הוא נקודת הכניסה לטסטים, ולכן הוא מפנה גם לעמודים שנוגעים בטסטים מזוויות אחרות, כך שמי שהגיע לכאן ראשון יוכל למצוא אותם בקלות.
Troubleshooting Guide – שגיאות ייבוא בזמן טסטים, בעיות event loop של asyncio, וכלים לדיבוג מהיר.
בדיקות ביצועים (Performance Tests) –
pytest -m performance: איך להריץ בבטחה, מה מסומן כקל ומה כבד, ואיפה נשמרים זמני הריצה.דוגמאות טסטים – Rate Limiting ואסינכרוניות – קטעי דוגמה לכתיבת טסטים ל-Rate Limiting מול Redis.
CI/CD Guide – אילו ג’ובים רצים על PR, ומה הסטטוסים הנדרשים.
הנחיות מלאות לסוכני AI – ההנחיות לסוכנים שעובדים בריפו; בפרק הטסטים:
tmp_pathבלבד לכל IO, ו-safe_rmtreeלמחיקות.