בדוגמא שראינו התחלנו עם קוד שקורא לקבצים. קבצים הם לא משהו מובנה, הם דינאמיים, יכולים לזוז, להשתנות ולהימחק. כל מערכת שנכתוב תהיה פגיעה לבאגים בעקבות קריאות לקבצים כמו כן גם הטסטים שנכתוב יהיו פגיעים לכך.
לכן קבצים הם גורם חיצוני שאסור לנו להסתמך עליהם בטסטים.
וככלל:
כל גורם חיצוני יהווה מכשול לטסטים, מכיוון שהוא עשוי להשתנות.
ומה רע בשינוי?
טסטים צריכים להיות דטרמיניסטיים.
ז”א שלא משנה מתי, איך ולמה, כאשר אריץ טסטים - עליהם לבצע ולבדוק את אותו הדבר בכל ריצה!
classPastryEveningDiscount(Discount): defIsActive(self): now = datetime.datetime.now() if now.hour > 18and now.hour < 23: returnTrue defDiscountPrice(self, category, CurrentPrice): if category == "Pastry": return CurrentPrice * 0.8#20% discount return CurrentPrice
הקוד הזה מבצע בדיקה למוצרים מהמאפייה ונותן 20 אחוז הנחה בערב. כעת נכתוב טסט לזה:
1 2 3 4 5 6 7 8
import dates
deftest_PastryEveningDiscount_IsActive(): discounter = dates.PastryEveningDiscount() isActive = discounter.IsActive() assert isActive, 'discount is not active'
מה הבעיה המיידית כאן? בגלל datetime.now הטסט אינו דטרמיניסטי. אם אני מריץ את הטסט בבוקר הטסט ייכשל, ובערב הוא ייעבור - זה לא מספיק טוב. (גם להריץ טסטים רק בבוקר או רק בערב זה לא מספיק טוב לכל החוכמולוגים).
בעקרון למה בכלל זה לא טוב? טסטים זו הוכחה לכך שהקוד שלנו עובד, אם טסט אינו דטרמיניסטי אז אין לנו הוכחה מספיק טובה שהקוד עובד. באגים בטסטים הם גרועים באותה מידה כמו באגים בקוד ולכן עלינו להיזהר!
mocking
כדי לבדוק את השעה הנוכחית נוכל להזריק פונקציה לקוד שלנו אשר יידע להחזיר מספר שמייצג את השעה:
נוכל לשכתב את הטסט כך שיהיה דטרמיניסטי, ועליו להוסיף גם טסט ליום:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import dates
deftest_PastryEveningDiscount_IsActive_DuringEvening(): eveningHour = 20# 0-24 hour format discounter = dates.PastryEveningDiscount(lambda : eveningHour) isActive = discounter.IsActive() assert isActive, 'discount is not active'
deftest_PastryEveningDiscount_IsActive_False_DuringDay(): eveningHour = 15# 0-24 hour format discounter = dates.PastryEveningDiscount(lambda : eveningHour) isActive = discounter.IsActive() # unittest.assertFalse(isActive) # במה שלמדנו בפרק 27 נוכל להשתמש בפונקציות מיוחדות לאסרטים assert isActive == False, 'discount is active during the day'
הטלת קובייה
במערכת למשחק תפקידים ווירטואלי יש הטלת קוביה אשר מחליטה על ה-Critical Hit. היא מוסיפה נקודות נזק נוספות כאשר זה מתבצע.
רנדומיזציה היא הכלי הכי לא דטרמיניסטי ולעיתים גם יותר מדי דטרמיניסטי. למה אני אומר את זה? השיטה שבה רוב הרנדומיזציה עובדת במחשבים היא בעזרת חישוב seed התחלתי, ואז שימוש בו ייגרום לייצור מספר רנדומלי מחושב. זאת אומרת - אם מביאים seed התחלתי זהה, גם התוצאות יהיו זהות.
classFakeCube: def__init__(self, number): self.__number = number
defThrow(self): returnself.__number
realCube = Cube() realCube.Throw()
fakeCube = FakeCube(4) fakeCube.Throw() #always 4 now
Dependency Injection
זה מוזכר בפרק הקודם בתור הזרקת תלויות מבחוץ. טכניקה זו מאפשרת לנו להכניס את הגורם המשתנה או שאינו דטרמיניסטי להיות כתלות מבחוץ.
בכך שאנו יוצרים הזרקה של התלות, אנו יכולים להזריק כל דבר שבא לנו. במידה ואם זו קוביה מזויפת, או פונקציה להחזרת שעה, או קורא אחר שאינו קורא מקובץ אלה ממחרוזת שאנו מביאים.
למעשה זה מביא לנו כלי עצום שבעזרתו נזייף התנהגות על מנת ליצור טסטים דטרמיניסטים ואמינים.
Mock & Stub
בפשטות מוק הוא כלי עוצמתי לזיוף מידע והתנהגות. סטאב הוא כלי פשטני יותר שמאפשר לזיוף חלקי או פשוט מידע חלקיץ.
ככללי נפריד בין השניים על ידי כך ש-Stub הוא פשטני, נבנה למטרה ספציפית ואינו דינאמי. למשל:
נלמד עכשיו בעזרת unittest.mock איך יוצרים, שולטים ועובדים במוקינג דינאמי.
תרגיל 1
א. מה זה Mock? ב. מה זה Stub? ג. מה ההבדל בינהם? ד. איך לבצע הזרקת תלויות?
תשובות
א
מוק הוא אובייקט היודע לזייף התנהגות, לעקוב אחריה וכך גם לבדוק פרמטרים שונים על ההתנהגות כמו כמה פעמים נקראה, עם אילו פרמטרים וכדו’
ב
סטאב הוא אובייקט פשטני יותר אשר יודע לזייף התנהגות ספציפית.
ג
ההבדל העיקרי הוא השימוש בהם, מוק יודע לעקוב אחר מצב האובייקט ובכך הוא מתאים לשימושים מסובכים של זיוף, סטאב מתאר מצב מסוים מאוד ולכן כאשר משתמשים בו, נשתמש בו למטרה אחת.
ד
ניתן להזריק תלות דרך הבנאי, דרך מתודה או אובייקט נוסף.
1 2 3 4 5
def__init(self, service): self.__service = service
defMyAction(service): service.Do()
או גם להזריק פונקציות:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
defMyAction(): return1
defMyAction2(): return2
defDo(action): val = action()
if val == 1: print("Hi") elif val == 2: print("Bye")
Do(MyAction) Do(MyAction2)
unittest.mock
ספריית המוק הזו הוכנסה לפייתון וכיום מגיעה עם הספרייה unittest.
defReturn(self, card): ifnot card inself.AllCards: raise IndexError('Trying to put card which doesnt exist') index = self.AllCards.index(card) if index notinself.PickedIndicies: raise IndexError('Trying to put card that wasnt picked') self.PickedIndicies.remove(index)
if(self.IsBigger(cardOne, cardTwo)): self.PlayerOnePoints += 1 self.CardPicker.Return(cardTwo) print(f"Player one won the round with {cardOne} over {cardTwo}") elif(self.IsBigger(cardTwo, cardOne)): self.PlayerTwoPoints += 1 self.CardPicker.Return(cardOne) print(f"Player Two won the round with {cardTwo} over {cardOne}")
defIsBigger(self, card, otherCard): firstStronger = self.StrongerSign in card andnotself.StrongerSign in otherCard if(firstStronger): returnTrue otherStronger = self.StrongerSign in otherCard andnotself.StrongerSign in card if otherStronger: returnFalse
המחלקה CardPicker עובדת בתור דק קלפים. ניתן לבחור קלף רנדומלי בעזרת Pick שהוא מחזיר קלף בצורת מחרוזת. כל קלף הוא בפורמט : sign_type למשל: 10_spades, K_diamonds, Jocker1…
במשחק הקלפים CardGame מתואר מצב המשחק וכל החוקים שלו:
המשחק בוחר סימן חזק יותר מבין ה-4 הקיימים במשחק - לב אדום, עלה שחור, תלתן שחור ויהלום אדום. לצבעים אין באמת משמעות.
כל שחקן בתורו לוקח קלף מהקופה
משווים את הקלפים ואצל מי שהכי חזק מקבל נקודה והו
גו’קר חזק יותר מהקלפים האחרים
אס הוא הכי חלש
הסימן החזק מנצח את כולם
במקרה של סימן חזק מול סימן חזק - בוחרים לפי סוג הקלף
הטסטים צריכים להתמקד בבדיקת החוקים של המשחק. ניתן להיעזר במאפיינים של המחלקה:
game = CardGame(picker) game.StrongerSign = 'clubs' game.TotalPointsNeeded = 10 picker.Pick.side_effect = ['K_spades', '5_hearts'] # first card, second card
res = game.Play()
assert res == ''# We don't have winner yet since we set it to 10 points assert game.PlayerOnePoints == 1 assert picker.Pick.call_count == 2 picker.Return.assert_called_once()
game = CardGame(picker) game.StrongerSign = 'clubs' game.TotalPointsNeeded = 10 picker.Pick.side_effect = ['Q_hearts', 'A_clubs'] # first card, second card
res = game.Play()
assert res == ''# We don't have winner yet since we set it to 10 points assert game.PlayerTwoPoints == 1 assert picker.Pick.call_count == 2 picker.Return.assert_called_once()
בנושא הבדיקות האחרון שלנו נלמד על כלי שנקרא pytest.cov.
זהו תוסף ל-pytest היודע לייצר כמות coverage לקוד שלנו.
מה זה coverage?
זהו מדד שבד”כ ניתן לראות באחוזים כמה מהקוד שלנו מכוסה על ידי טסטים. הוא יודע לזהות לאילו תנאים נכנסו, איזה לולאות רצו ואיזה פונקציות הגיבו.
כדי להתקין אותו נשתמש ב: pip install pytest.cov
נסדר את התיקייה כך שיהיה לנו קובץ טסטים ואת קבצי הפייתון שלנו:
לאחר מכן נריץ בעזרת pytest את ה-cov: pytest --cov --cov-report=html test_cards.py
שימו לב שהשתמשנו בתוצאת html המראה לנו באופן גראפי ויפה את הקוד שלנו: הטסט מייצר index.html בתוך תיקיית testcov.
אם פותחים את זה, זה נראה כך:
נוכל להיכנס לכל אחד מהקבצים ולראות מה רץ ומה לא:
תרגיל 4
הריצו את הכלי coverage על הטסטים שכתבתם ותראו מה ה-coverage שקיבלתם! האם יש מקרה קצה ששכחתם לבדוק?
ביצוע mock היא טכניקה חשובה בעולם התוכנה, היא מקלה עלינו מלדאוג להרבה פרמטרים בזמן טסט ומאפשר לנו לזהות בקלות יותר בעיות, ולזייף יותר התנהגויות. מה שמאפשר לנו לבדוק חלקים יותר ויותר קטנים במערכת שלנו. כפי ששמתם לב - זה גם עזר לנו לבצע החלטות עיצוביות יותר טובות, המביאות לחשיבה מעמיקה יותר לגבי המערכת.