פרק קודם:
פייתון 26 - לינטרים ופורמטבאג זו התנהגות תוכנתית לא רצויה.
המילה Bug
היא אבולוציה של המילה Bugge
שמקורה בשתי מילים אחרות מגרמנית.
שתי המילים הן Bugbear
ו-Bugaboo
.
שמתורגמות בערך כ-“גרמלין”.
הכוונה לחיה/יישות קוסמית שעושה צרות.
פעם היו משתמשים בה בעיקר במכניקה כדי לתאר בעיות קטנות.
באגים בתוכנה ובמוצרים צצים ונעלמים - חלקם חמורים, חלקם פחות.
לפני שניגש לבדיקות אני רוצה להכיר לכם שני סיפורים מפורסמים על באגים.
באג אחד הוביל לקריסה כלכלית.
השני גרם להרג של מטופלים.
כמעט כל מתכנת שמע על המקרה המצער שחברה איבדה את עצמה בפחות מ-24 שעות.
החברה איבדה כמעט ברגעים 460 מיליון דולר!
כיצד זה קרה?
במערכת החברתית שלהם לסחר במניות היה קוד ישן שבדרך כלל לא עבד - כי הוא היה מכובה.
שינוי בתוכנה גרם לכך שהחלק במערכת הזו ייעבוד - מערכת שעיקרה הייתה לבצע בדיקות.
אך כאשר מערכת כזו רצה בצורה מבצעית (הרצה מבצעית = הרצה בעולם האמיתי) היא קנתה ומכרה מניות בצורה מאוד מהירה כאשר כל מכירה איבדה כמה סנטים מערך המניה - כאשר מתבצעים 4 מיליון העברות כאלו - החברה מאבדת כסף בצורה אסטרונומית.
אם לאבד את החברה לא היה מספיק - היא נקנסה בעוד 12 מיליון דולר בגלל בעיות רגולציה.
מערכת לטיפול בחולי סרטן שעובדת בעזרת ירי של קרינה אלגטרומגנטית הייתה פגיעה לבאג חמור בגלל החלפת חומרה וגרמה למותם של כמה אנשים ופציעה חמורה במטופלים אחרים.
הבאג קרה כתוצאה משימוש לא נכון בתהליכונים בתוך החומרה - כמה תהליכונים עבדו בו זמנית ושינו פרמטרים - כך שגרמו למכונה לירות קרינה מקסימלית מה שפוגע במערכת החיים.
שני הבאגים האלו חמורים מכיוון שאחד מהם הוביל לקריסה כלכלית והשני להרג אנשים - קוד ומערכות שנראות טריוויאליות יכולות בסופו של דבר לגרום לטעויות כתוצאה מכדור שלג מתגלגל.
בשביל למנוע טעויות כאלו - כלל המתכנתים לומדים כיצד לכתוב בדיקות כדי להגן על הקוד מבאגים ולהוריד את פוטנציאל הבאגים ל-1-4%!
אין כיום מערכת ללא באגים - אך כל מערכת צריכה לעמוד ביעדים שעיצבו עבורה, בעזרת עבודה תכנון מדוייקת ניתן למנוע באגים בשטח.
בדיקות הן אימות לתקינות התנהגות הקוד.
התהליך המלא לאימות התוכנה מורכב מכמה תהליכים וסנכרון הידע כדי שהתנהגות התוכנה תהיה מיטבית.
לעיתים נקרא למקרה בדיקה “טסט”.
זה מתחיל ברמת אפיון המערכת כאשר שואלים שאלות על איך המערכת אמורה להתנהג ולהיראות.
למשל בבאג במערכת הפיננסית היינו יכולים לשאול - האם האסטרטגיה לקנייה ומכירה של מניות מניבה רווחים?
בעולם מתוקן היינו מצפים להרצה “יבשה” של המערכת על מנת לראות אם יש רווחים - לתהליך הזה אנחנו קוראים בדיקות.
“הרצה יבשה” אומר שאנחנו לא מכניסים נתונים אמיתיים או מריצים את הקוד בסביבה האמיתית שתגרום לנו לאיבוד כסף.
ורק לאחר תהליך נוקשה לאימות הקוד - נוכל לשחרר את הקוד לטבע.
בפסודו קוד היינו יכולים לנסח את זה באופן הבא:
1 | strategy = NewStrategy() |
כמובן שהדוגמא הזו היא סופר מפושטת על מנת להבין את התהליך.
וכמו שרואים זהו תהליך ששואלים בו שאלות - האם הרווחים אחרי הקנייה של המניות גדולים מ-0, זאת אומרת האם רווחנו פה כסף.
במקרה של המערכת הרפואית - לומר לזכותם היה מאוד קשה לעלות על באג כזה מכיוון שזהו גם כשל מכני של המערכת.
אך בשביל זה יש לוודא שכלל הרכיבים עובדים כמצופה בכל המצבים.
אנו קוראים למצבים כאלו מצבי קיצון
.
אלו מקרי קצה שקשה לעלות עליהם ולכן גם קשה לבדוק אותם.
וככל הנראה אם אנשים לא היו מתים גם לא היו עולים על הבאג הזה!
בדיקות תוכנה מתחלקות ע”פ היקף הרכיבים.
ככל שעולים בהיקף כך גם העלות והזמן שנשקיע בבדיקות גדלה.
גם ככל שנשקיע בבדיקות כך גם איכות התוכנה שלנו תעלה והסיכוי לבאגים ייקטן!
בפוסט הנ”ל נלמד איך לבצע בדיקות בהיקף 1 ו-2.
ז”א נלמד לכתוב בדיקות לרכיבים שלנו, ובדיקות למערכות שלנו.
בשביל לבצע בדיקות נכיר 2 מתודולוגיות שעוזרות לנו להציב מטרות:
TDD
- Test driven design/development. BDD
- Behavior driven design/development.לפני כל קוד שנכתוב יש לכתוב מקרה בדיקה.
למשל - המשימה שלנו היא לממש פונקציית חיבור.
במתודולוגיית ה-TDD
נכתוב קודם כל מקרה בדיקה ולא מימוש החיבור.
1 | def Add(a,b): |
הבדיקה תבדוק אם פונקציית החיבור תיתן לנו את התוצאה הרצויה.
שימו לב שערך ה-expected
הוא התוצאה ולא פעולת חיבור בפני עצמה.
אין לכתוב:
1 | def TestAdd(add): |
הסיבה לכך שאם נכתוב את אותו קוד שהפונקציה מבצעת - אז אנחנו לא באמת בודקים אותה אלה מחקים אותה.
במקום זה יש ליצור מידע שהוא יהיה התוצאה.
במקרים פשוטים יהיה קל לייצר את המידע הזה.
במקרה מסובכים יותר - זה יהיה מורכב יותר.
בשיטה הזו מצופה לכתוב מקרה בדיקה שנכשל.
ורק לאחר מימוש המתודה - נראה שהוא עובר.
1 | def Add(a,b): |
המתודולוגיה הזו נבנתה על גבי ה-TDD
.
במקום מקרי בדיקה - ההתנהגות היא במרכז.
בצורה תיאורית ניתן לומר מה המערכת עושה וכיצד היא אמורה להתנהג.
בשיטה הזו נגדיר כמה פרמטרים כמקרי בדיקה:
למשל עבור ניהול מחסן ספרים:
1 | נושא: הוספה של ספרים למחסן |
שימו לב לתצורה כאן שניתן לחזור עליה:
1 | כותרת |
בתצורה הזו אנחנו יכולים להגדיר במדויק התנהגות ללא קשר לפרטים הטכניים של המערכת.
אין לנו כאן ידע על המימוש - לא מסד הנתונים, לא התקשורת ולא המבנים.
יש ליצור שמות תיאוריים שמדייקים במה שמתבצע או בתפקידם.
שמות טסטים בדרך כלל מתחילים ב-test_
אך בקונבנציה ניתן לא להוסיף את התחילית הזו.
שם הפונקצייה צריך להיות מדוייק, אני משתמש במודל:test_function_parameters_results
.
למשל:test_div_onzero_throws
השם של הטסט חשוב כדי להבין מה הטסט עושה ואילו אילוצים קיימים שם.
כאשר מצטברים מקרי בדיקה הטסטר ייכתוב את שמותיהם וע”פ שמות אינדיקטיביים נוכל בדיוק להבין איפה הבאג.
למשל:
1 | div_onzero_throws PASS |
גוף הטסט בנוי מ-3 שלבים.
1 | def test_do_returns_zero_on_success(): |
אם יש לנו קוד משוכפל בין טסטים זה בסדר.
כל טסט מגדיר את תחום האחריות שלו.
לעיתים ניתן להמיר קוד משוכפל לפונקציה הניתנת לשימוש חוזר.
אך אם זה מורכב מדי - לא חייבים לבצע את זה.
אחת מהמטרות של טסטים טובים זה לתעד את השימושיות במערכת.
כאשר מישהו לומד על המערכת הוא יירצה להבין איך המערכת מתנהגת וטסטים יכולים לתאר בצורה מיטבית את ההתנהגות.
ולכן - על הטסטים להיות קלים לקריאה והבנה.
טסטים צריכים להיות כמה שיותר מבודדים.
הרצה מבודדת של הבדיקות לא תשפיע על המערכת באופן כללי מה שייאפשר להריץ הרבה מקרי בדיקה בבת אחת ובמקביל.
טסטים טובים הם מהירים וניתן להריץ אותם בכל זמן.
זה מאפשר להוכיח את התנהגות המערכת לפני ואחרי כל שינוי במערכת.
סיכום העקרונות:
אחת הסיבות לפופולריות של פייתון היא הקלות בה לכתוב בדיקות.unittest
זוהי ספרייה מובנית בפייתון לבדיקות.
Fixture
הוא איגוד של בדיקות המצריכות הכנה מוקדמת של סביבת הבדיקות וקוד ניקוי.
למשל אם הקוד שלנו מצריך תיקייה במחשב אז ה-Fixture
יידאג ליצור את התיקייה ולנקות אותה בסוף ההרצה.
מקרה בדיקה יחיד.
איגוד של מקרי בדיקה - Test Case
או כמה איגודים - Test Suite
.
יכול להיות אחד מהם או שניהם.
מנהל ומריץ את כלל הטסטים.
כדי לכתוב טסט יש לרשת את המחלקה unittest.TestCase
.
ולאחר מכן להוסיף פונקציה עם התחילית test_
.
שימו לב שב-pytest
צריך להוסיף את התחילית.
1 | import unittest |
ניתן להריץ את הקובץ באופן רגיל:py tests.py
או בעזרת גילוי אוטומטי:python -m unittest discover
python -m unittest test_module.MathTests.test_add
python -m unittest tests/my_tests.py
בתצורה הזו הוא יינסה לגלות טסטים לבד.
python -m unittest
נוח במידה ואנו רוצים להריץ טסטים מסוימים או טסט ספיצפי.
ניתן להשתמש ב--k
כדי לפלטר על פי שם:python -m unittest -k test*
הפקודה הזו תריץ את כלל הטסטים שמתחילים בשם test
.
בטסט הבודד שיצרנו השתמשנו ב-assertEqual
כדי לבדוק שהערכים שווים.
פונקציות assert
אלו פונקציות שבודקות את התוצאה על פי קריטריון מסוים.
על פי התוצאה הוא יכריע אם הטסט עבר או לא.
בדוגמא שלנו self.assertEqual(result,expected)
יכריע אם פעולת החיבור הצליחה או לא.
כאשר הטסט עובר זה ידפיס:
1 | ---------------------------------------------------------------------- |
כאשר הוא לא עובר זה ידפיס:
1 | F |
יבדוק אם שני אובייקטים זהים.
יבדוק אם האובייקט הוא True
.
יבדוק אם האובייקט הוא False
.
יבדוק אם קטע הקוד זורק שגיאה.
1 | def do(): |
ניתן לממש את הפונקציות setUp
ו-tearDown
על מנת לבצע איתחול וניקוי לכל test case
.
הספרייה מחפשת פונקציות בצורה אוטומטית המתחילות עם המילה test_
.
כך שהפונקציה test_add(self)
תהיה פונקציה לבדיקה.
הפונקציות האלו ירוצו לפני כל טסט ואחרי כל טסט.
על מנת ליצור setUp
יחיד ניתן להשתמש בפונקציות setUpClass
ו-tearDownClass
.
1 | class MathTests(unittest.TestCase): |
פונקציות אתחול וניקוי לכל ה-מודול.
מממשים אותם כפונקציות רגילות במודול:
1 | def setUpModule(): |
כתבו טסטים לפעולות המתמטיות:
שימו לב למקרי הבדיקה - יש לבדוק שחילוק ב-0 זורק!
1 |
|
pytest
היא ספרייה נוספת לא מובנית בפייתון אשר מתפארת בקלות שלה לשימוש.
לכתוב ולהריץ טסטים נעשת בצורה מאוד פשטנית, כמו שלמדנו על התחיליות גם כאן יש שימוש בהן:
1 | import pytest |
מריצים בעזרת pytest
:pytest myTests.py
.
גם ל-pytest
קיימת פונקציה לתפיסת שגיאות בטסטים:
1 | def test_zero_division(): |
ניתן גם להשתמש בדקורטור:
1 | import pytest |
היתרונות בספריה הזו:
assert
של פייתון. unittest
.היתרונות ב-unittest
:
OOP
מה שמאפשר סדר יותר מוחלט. assert
ים מובנים לספרייה.לעיתים נרצה לבצע טסט מספר פעמים עם גורמים שונים.
1 | def test_Add_With(a,b, result): |
ואז נוכל לבדוק:
1 | test_Add_With(1,2,3) |
בעזרת הדקורטור Parametrized
נוכל ליישם זאת:
1 | import pytest |
בפרמטר הראשון אנו מציינים איך הפרמטר בנוי - יש לנו 2 גורמים ותוצאה
ובפרמטר השני אנו מעבירים רשימה של טאפלים כפרמטר.
כל טאפל הוא טסט ייחודי משלו.
לא מספיק רק לכתוב בדיקות לקוד שלנו - יש לבצע ניתוח פעיל על הקוד שלנו כדי לוודא שניתן לבדוק אותו בכלל!
לעיתים קוד שמסתמך על פרטים ספציפיים לא ניתן לבדוק אותו.
כמו למשל ה-date.now()
.
הקוד הנ”ל יהיה קשה לבדיקה:
1 | from datetime import date |
כדי לאפשר לבדוק את הקוד יש צורך באבסטרקציה ל-date.today()
על מנת לשלוט ביום שמוחזר.
ניתן בקלות ליצור מחלקה להתנהגות הזו:
1 | class DateTime: |
כעת ניתן להשתמש ב-DateTime
משלנו:
1 | class MyScheduler: |
לעיתים קוד שמאוגד לפונקציות ארוכות או פעולות רבות לא יכול להיבדק כראוי ויש הרבה תנאים כדי שנוכל לבדוק אותו.
למשל יש לנו פונקציה במחלקה אשר לוקחת נתונים ממקור חיצוני, מבצעת חישוב ומחזירה בהתאם לתנאי את כל המידע.
1 | class DeriveData: |
על מנת לבדוק את DeriveData.Get
עם פונקציה אשר מחפשת על כל רשומה צריך מקור חיצוני כדי לעבוד איתו.
זה מפר את עקרון הסביבה המבודדת!
מה שצריך לעשות כדי לאפשר לבדוק את הפונקציה הזו היא לפצל את הקוד!
1 | class DeriveData: |
עכשיו נוכל להזריק את המידע data
מבחוץ ולאפשר לבדוק את היחידה הזו בפרטניות.
אם היחידה תלויה במידע או התנהגות חיצונית - נעדיף להזריק אותה מבחוץ מאשר בפנים.
1 | class DataProvider: |
הבעיה כאן היא השורות האלו:
1 | def __init__(self): |
במקום ליצור את המחלקה בפנים כדאי לקבל אותה מבחוץ בתצורה הזו:
1 | def __init__(self, dataProvider): |
כך שנוכל להזריק התנהגות שונה.
תצורה זו נקראת Stub
או Mock
.
במקום להזריק התנהגות אמיתית מזריקים התנהגות מזויפת שמתנהגת כמו שאנחנו רוצים.
למשל אם נרצה להזריק DataProvider
משלנו נוכל לרשת את המחלקה ולהחזיר משהו אחר:
1 | import pytest |
Stub
הוא אובייקט פשוט אשר מטרתו להחזיר ערך שאנחנו מצפים לו.Mock
לעומתו הוא אובייקט יותר מורכב אשר אפשר לשלוט בהגדרות.
נלמד בפרק הבא יותר על מוקים וכיצד להגדיר אותם!
unittest
, כיצד מגדירים אתחול וסיום אחרי כל טסט בודד? unittest
, כיצד מגדירים אתחול וסיום אחרי כל Fixture
? unittest
, כיצד מגדירים אתחול וסיום אחרי כל מודול? ניתן להיעזר בפרק על json
כדי להבין את ההתנהגות:
1 | import json |
1 | from datetime import date |
setUp
ו-tearDown
.
setUpClass
ו-tearDownClass
.
setUpModule
ו-tearDownModule
.
כאן השתמשנו ב-pytest
.
1 | import json |
1 | from datetime import date |
ההבדל בין מתכנת למהנדס תוכנה הוא היכולת לתכנן ולכתוב אפיונים.
חלק מתהליך העיצוב הוא גם תהליך הבדיקות - כיצד בכלל נבדוק שהתוכנה שלנו עובדת כראוי.
ללא ספק זהו תהליך מורכב וחשוב לא פחות מכתיבת מימוש המערכת ואם לכל מוצר בעולם יהיו בדיקות מקיפות, נספק ללקוחות ולעצמינו מערכות בטוחות, אמינות ויעילות!
פרק הבא:
פייתון 28 - בדיקות - זיוף התנהגות