פרק קודם:
פייתון 27 - לינטרים ופורמטמה נלמד
- מה הם באגים
- מה הם בדיקות
- תכנון תהליך הבדיקות
- ספריית unittest
- ספריית pytest
- עקרונות לטסטביליות
באגים
באג זו התנהגות תוכנתית לא רצויה.
המילה Bug
היא אבולוציה של המילה Bugge
שמקורה בשתי מילים אחרות מגרמנית.
שתי המילים הן Bugbear
ו-Bugaboo
.
שמתורגמות בערך כ-“גרמלין”.
הכוונה לחיה/יישות קוסמית שעושה צרות.
פעם היו משתמשים בה בעיקר במכניקה כדי לתאר בעיות קטנות.
באגים בתוכנה ובמוצרים צצים ונעלמים - חלקם חמורים, חלקם פחות.
לפני שניגש לבדיקות אני רוצה להכיר לכם שני סיפורים מפורסמים על באגים.
באג אחד הוביל לקריסה כלכלית.
השני גרם להרג של מטופלים.
Knight Capital Group
כמעט כל מתכנת שמע על המקרה המצער שחברה איבדה את עצמה בפחות מ-24 שעות.
החברה איבדה כמעט ברגעים 460 מיליון דולר!
כיצד זה קרה?
במערכת החברתית שלהם לסחר במניות היה קוד ישן שבדרך כלל לא עבד - כי הוא היה מכובה.
שינוי בתוכנה גרם לכך שהחלק במערכת הזו ייעבוד - מערכת שעיקרה הייתה לבצע בדיקות.
אך כאשר מערכת כזו רצה בצורה מבצעית (הרצה מבצעית = הרצה בעולם האמיתי) היא קנתה ומכרה מניות בצורה מאוד מהירה כאשר כל מכירה איבדה כמה סנטים מערך המניה - כאשר מתבצעים 4 מיליון העברות כאלו - החברה מאבדת כסף בצורה אסטרונומית.
אם לאבד את החברה לא היה מספיק - היא נקנסה בעוד 12 מיליון דולר בגלל בעיות רגולציה.
Therac-25
מערכת לטיפול בחולי סרטן שעובדת בעזרת ירי של קרינה אלגטרומגנטית הייתה פגיעה לבאג חמור בגלל החלפת חומרה וגרמה למותם של כמה אנשים ופציעה חמורה במטופלים אחרים.
הבאג קרה כתוצאה משימוש לא נכון בתהליכונים בתוך החומרה - כמה תהליכונים עבדו בו זמנית ושינו פרמטרים - כך שגרמו למכונה לירות קרינה מקסימלית מה שפוגע במערכת החיים.
שני הבאגים האלו חמורים מכיוון שאחד מהם הוביל לקריסה כלכלית והשני להרג אנשים - קוד ומערכות שנראות טריוויאליות יכולות בסופו של דבר לגרום לטעויות כתוצאה מכדור שלג מתגלגל.
בשביל למנוע טעויות כאלו - כלל המתכנתים לומדים כיצד לכתוב בדיקות כדי להגן על הקוד מבאגים ולהוריד את פוטנציאל הבאגים ל-1-4%!
אין כיום מערכת ללא באגים - אך כל מערכת צריכה לעמוד ביעדים שעיצבו עבורה, בעזרת עבודה תכנון מדוייקת ניתן למנוע באגים בשטח.
בדיקות קוד
בדיקות הן אימות לתקינות התנהגות הקוד.
התהליך המלא לאימות התוכנה מורכב מכמה תהליכים וסנכרון הידע כדי שהתנהגות התוכנה תהיה מיטבית.
לעיתים נקרא למקרה בדיקה “טסט”.
זה מתחיל ברמת אפיון המערכת כאשר שואלים שאלות על איך המערכת אמורה להתנהג ולהיראות.
למשל בבאג במערכת הפיננסית היינו יכולים לשאול - האם האסטרטגיה לקנייה ומכירה של מניות מניבה רווחים?
בעולם מתוקן היינו מצפים להרצה “יבשה” של המערכת על מנת לראות אם יש רווחים - לתהליך הזה אנחנו קוראים בדיקות.
“הרצה יבשה” אומר שאנחנו לא מכניסים נתונים אמיתיים או מריצים את הקוד בסביבה האמיתית שתגרום לנו לאיבוד כסף.
ורק לאחר תהליך נוקשה לאימות הקוד - נוכל לשחרר את הקוד לטבע.
בפסודו קוד היינו יכולים לנסח את זה באופן הבא:
1 | strategy = NewStrategy() |
כמובן שהדוגמא הזו היא סופר מפושטת על מנת להבין את התהליך.
וכמו שרואים זהו תהליך ששואלים בו שאלות - האם הרווחים אחרי הקנייה של המניות גדולים מ-0, זאת אומרת האם רווחנו פה כסף.
במקרה של המערכת הרפואית - לומר לזכותם היה מאוד קשה לעלות על באג כזה מכיוון שזהו גם כשל מכני של המערכת.
אך בשביל זה יש לוודא שכלל הרכיבים עובדים כמצופה בכל המצבים.
אנו קוראים למצבים כאלו מצבי קיצון
.
אלו מקרי קצה שקשה לעלות עליהם ולכן גם קשה לבדוק אותם.
וככל הנראה אם אנשים לא היו מתים גם לא היו עולים על הבאג הזה!
תכנון בדיקות ואימות התנהגות
בדיקות תוכנה מתחלקות ע”פ היקף הרכיבים.
- היקף הרכיב הקטן - למשל פונקציה או מחלקה.
- בדיקות מערכתיות - בדיקה שרכיבים בתוך המערכת עובדים.
- בדיקות אינטגרציה - חיבור בין מערכות ושרתים, למשל איך ה-UI עובד ביחד עם המערכת.
- בדיקות מקיפות - בדיקות בתצורות שונות כדי למצוא מקרי קצה שונים.
- בדיקות A/B - בדיקות בתצורה מסוימת על מנת להשוות התנהגות.
- בדיקות ביצועים - בדיקות מכל סוג שהוא על מנת למדוד ביצועים ויעילות המערכות.
ככל שעולים בהיקף כך גם העלות והזמן שנשקיע בבדיקות גדלה.
גם ככל שנשקיע בבדיקות כך גם איכות התוכנה שלנו תעלה והסיכוי לבאגים ייקטן!
בפוסט הנ”ל נלמד איך לבצע בדיקות בהיקף 1 ו-2.
ז”א נלמד לכתוב בדיקות לרכיבים שלנו, ובדיקות למערכות שלנו.
בשביל לבצע בדיקות נכיר 2 מתודולוגיות שעוזרות לנו להציב מטרות:
TDD
- Test driven design/development.BDD
- Behavior driven design/development.
TDD - Test Driven Design
לפני כל קוד שנכתוב יש לכתוב מקרה בדיקה.
למשל - המשימה שלנו היא לממש פונקציית חיבור.
במתודולוגיית ה-TDD
נכתוב קודם כל מקרה בדיקה ולא מימוש החיבור.
1 | def Add(a,b): |
הבדיקה תבדוק אם פונקציית החיבור תיתן לנו את התוצאה הרצויה.
שימו לב שערך ה-expected
הוא התוצאה ולא פעולת חיבור בפני עצמה.
אין לכתוב:
1 | def TestAdd(add): |
הסיבה לכך שאם נכתוב את אותו קוד שהפונקציה מבצעת - אז אנחנו לא באמת בודקים אותה אלה מחקים אותה.
במקום זה יש ליצור מידע שהוא יהיה התוצאה.
במקרים פשוטים יהיה קל לייצר את המידע הזה.
במקרה מסובכים יותר - זה יהיה מורכב יותר.
בשיטה הזו מצופה לכתוב מקרה בדיקה שנכשל.
ורק לאחר מימוש המתודה - נראה שהוא עובר.
1 | def Add(a,b): |
BDD - Behavior Driven Design
המתודולוגיה הזו נבנתה על גבי ה-TDD
.
במקום מקרי בדיקה - ההתנהגות היא במרכז.
בצורה תיאורית ניתן לומר מה המערכת עושה וכיצד היא אמורה להתנהג.
בשיטה הזו נגדיר כמה פרמטרים כמקרי בדיקה:
- מי מבצע
- סיטואציות המדמות התנהגות
למשל עבור ניהול מחסן ספרים:
1 | נושא: הוספה של ספרים למחסן |
שימו לב לתצורה כאן שניתן לחזור עליה:
1 | כותרת |
בתצורה הזו אנחנו יכולים להגדיר במדויק התנהגות ללא קשר לפרטים הטכניים של המערכת.
אין לנו כאן ידע על המימוש - לא מסד הנתונים, לא התקשורת ולא המבנים.
עקרונות כתיבת טסטים
שמות תיאוריים
יש ליצור שמות תיאוריים שמדייקים במה שמתבצע או בתפקידם.
שמות טסטים בדרך כלל מתחילים ב-test_
אך בקונבנציה ניתן לא להוסיף את התחילית הזו.
שם הפונקצייה צריך להיות מדוייק, אני משתמש במודל:test_function_parameters_results
.
למשל:test_div_onzero_throws
השם של הטסט חשוב כדי להבין מה הטסט עושה ואילו אילוצים קיימים שם.
כאשר מצטברים מקרי בדיקה הטסטר ייכתוב את שמותיהם וע”פ שמות אינדיקטיביים נוכל בדיוק להבין איפה הבאג.
למשל:
1 | div_onzero_throws PASS |
Assign - Act - Assert
גוף הטסט בנוי מ-3 שלבים.
- יצירת ערכים ואתחולם
- קריאה לפונקציה
- בדיקה שהערכים תקינים
1 | def test_do_returns_zero_on_success(): |
כפילויות זה בסדר
אם יש לנו קוד משוכפל בין טסטים זה בסדר.
כל טסט מגדיר את תחום האחריות שלו.
לעיתים ניתן להמיר קוד משוכפל לפונקציה הניתנת לשימוש חוזר.
אך אם זה מורכב מדי - לא חייבים לבצע את זה.
קריאות
אחת מהמטרות של טסטים טובים זה לתעד את השימושיות במערכת.
כאשר מישהו לומד על המערכת הוא יירצה להבין איך המערכת מתנהגת וטסטים יכולים לתאר בצורה מיטבית את ההתנהגות.
ולכן - על הטסטים להיות קלים לקריאה והבנה.
סביבה מבודדת
טסטים צריכים להיות כמה שיותר מבודדים.
הרצה מבודדת של הבדיקות לא תשפיע על המערכת באופן כללי מה שייאפשר להריץ הרבה מקרי בדיקה בבת אחת ובמקביל.
הרצה חוזרת ומהירה
טסטים טובים הם מהירים וניתן להריץ אותם בכל זמן.
זה מאפשר להוכיח את התנהגות המערכת לפני ואחרי כל שינוי במערכת.
סיכום העקרונות:
- תתארו את הטסטים בשפה מדויקת.
- שיטת ה-AAA, Assign, Act, Assert.
- כפיליות זה בסדר
- הסביבה צריכה להיות מובדדת.
- טסטים צריכים להיות קריאים.
- ניתן לחזור על טסטים ולהריץ אותם במהירות.
unittest
אחת הסיבות לפופולריות של פייתון היא הקלות בה לכתוב בדיקות.unittest
זוהי ספרייה מובנית בפייתון לבדיקות.
Test Fixture
Fixture
הוא איגוד של בדיקות המצריכות הכנה מוקדמת של סביבת הבדיקות וקוד ניקוי.
למשל אם הקוד שלנו מצריך תיקייה במחשב אז ה-Fixture
יידאג ליצור את התיקייה ולנקות אותה בסוף ההרצה.
Test Case
מקרה בדיקה יחיד.
Test Suite
איגוד של מקרי בדיקה - Test Case
או כמה איגודים - Test Suite
.
יכול להיות אחד מהם או שניהם.
Test Runner
מנהל ומריץ את כלל הטסטים.
כתיבת Unit test
כדי לכתוב טסט יש לרשת את המחלקה 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
.
בדיקת תוצאות - assert
בטסט הבודד שיצרנו השתמשנו ב-assertEqual
כדי לבדוק שהערכים שווים.
פונקציות assert
אלו פונקציות שבודקות את התוצאה על פי קריטריון מסוים.
על פי התוצאה הוא יכריע אם הטסט עבר או לא.
בדוגמא שלנו self.assertEqual(result,expected)
יכריע אם פעולת החיבור הצליחה או לא.
כאשר הטסט עובר זה ידפיס:
1 | ---------------------------------------------------------------------- |
כאשר הוא לא עובר זה ידפיס:
1 | F |
assertEquals
יבדוק אם שני אובייקטים זהים.
assertTrue
יבדוק אם האובייקט הוא True
.
assertFalse
יבדוק אם האובייקט הוא False
.
assertRaises
יבדוק אם קטע הקוד זורק שגיאה.
1 | def do(): |
setUp ו-tearDown
ניתן לממש את הפונקציות setUp
ו-tearDown
על מנת לבצע איתחול וניקוי לכל test case
.
הספרייה מחפשת פונקציות בצורה אוטומטית המתחילות עם המילה test_
.
כך שהפונקציה test_add(self)
תהיה פונקציה לבדיקה.
setUpClass ו-tearDownClass
הפונקציות האלו ירוצו לפני כל טסט ואחרי כל טסט.
על מנת ליצור setUp
יחיד ניתן להשתמש בפונקציות setUpClass
ו-tearDownClass
.
1 | class MathTests(unittest.TestCase): |
setUpModule ו-tearDownModule
פונקציות אתחול וניקוי לכל ה-מודול.
מממשים אותם כפונקציות רגילות במודול:
1 | def setUpModule(): |
תרגול
כתבו טסטים לפעולות המתמטיות:
- חיסור
- כפל
- חילוק
- חזקה
שימו לב למקרי הבדיקה - יש לבדוק שחילוק ב-0 זורק!
1 |
|
Pytest
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 גורמים ותוצאה
ובפרמטר השני אנו מעבירים רשימה של טאפלים כפרמטר.
כל טאפל הוא טסט ייחודי משלו.
עקרונות לטסטביליות - Testability
לא מספיק רק לכתוב בדיקות לקוד שלנו - יש לבצע ניתוח פעיל על הקוד שלנו כדי לוודא שניתן לבדוק אותו בכלל!
אבסטרקציה
לעיתים קוד שמסתמך על פרטים ספציפיים לא ניתן לבדוק אותו.
כמו למשל ה-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 |
ההבדל בין מתכנת למהנדס תוכנה הוא היכולת לתכנן ולכתוב אפיונים.
חלק מתהליך העיצוב הוא גם תהליך הבדיקות - כיצד בכלל נבדוק שהתוכנה שלנו עובדת כראוי.
ללא ספק זהו תהליך מורכב וחשוב לא פחות מכתיבת מימוש המערכת ואם לכל מוצר בעולם יהיו בדיקות מקיפות, נספק ללקוחות ולעצמינו מערכות בטוחות, אמינות ויעילות!
פרק הבא:
פייתון 29 - בדיקות - זיוף התנהגות