13 min. read

פרק קודם:

פייתון 26 - לינטרים ופורמט

מה נלמד

  • מה הם באגים
  • מה הם בדיקות
  • תכנון תהליך הבדיקות
  • ספריית unittest
  • ספריית pytest
  • עקרונות לטסטביליות

באגים

באג זו התנהגות תוכנתית לא רצויה.

באגים בתוכנה ובמוצרים צצים ונעלמים - חלקם חמורים, חלקם פחות.
לפני שניגש לבדיקות אני רוצה להכיר לכם שני סיפורים מפורסמים על באגים.
באג אחד הוביל לקריסה כלכלית.
השני גרם להרג של מטופלים.

Knight Capital Group

כמעט כל מתכנת שמע על המקרה המצער שחברה איבדה את עצמה בפחות מ-24 שעות.
החברה איבדה כמעט ברגעים 460 מיליון דולר!

כיצד זה קרה?

במערכת החברתית שלהם לסחר במניות היה קוד ישן שבדרך כלל לא עבד - כי הוא היה מכובה.
שינוי בתוכנה גרם לכך שהחלק במערכת הזו ייעבוד - מערכת שעיקרה הייתה לבצע בדיקות.
אך כאשר מערכת כזו רצה בצורה מבצעית (הרצה מבצעית = הרצה בעולם האמיתי) היא קנתה ומכרה מניות בצורה מאוד מהירה כאשר כל מכירה איבדה כמה סנטים מערך המניה - כאשר מתבצעים 4 מיליון העברות כאלו - החברה מאבדת כסף בצורה אסטרונומית.

אם לאבד את החברה לא היה מספיק - היא נקנסה בעוד 12 מיליון דולר בגלל בעיות רגולציה.

Therac-25

מערכת לטיפול בחולי סרטן שעובדת בעזרת ירי של קרינה אלגטרומגנטית הייתה פגיעה לבאג חמור בגלל החלפת חומרה וגרמה למותם של כמה אנשים ופציעה חמורה במטופלים אחרים.

הבאג קרה כתוצאה משימוש לא נכון בתהליכונים בתוך החומרה - כמה תהליכונים עבדו בו זמנית ושינו פרמטרים - כך שגרמו למכונה לירות קרינה מקסימלית מה שפוגע במערכת החיים.


שני הבאגים האלו חמורים מכיוון שאחד מהם הוביל לקריסה כלכלית והשני להרג אנשים - קוד ומערכות שנראות טריוויאליות יכולות בסופו של דבר לגרום לטעויות כתוצאה מכדור שלג מתגלגל.

בשביל למנוע טעויות כאלו - כלל המתכנתים לומדים כיצד לכתוב בדיקות כדי להגן על הקוד מבאגים ולהוריד את פוטנציאל הבאגים ל-1-4%!
אין כיום מערכת ללא באגים - אך כל מערכת צריכה לעמוד ביעדים שעיצבו עבורה, בעזרת עבודה תכנון מדוייקת ניתן למנוע באגים בשטח.


בדיקות קוד

בדיקות הן אימות לתקינות התנהגות הקוד.
התהליך המלא לאימות התוכנה מורכב מכמה תהליכים וסנכרון הידע כדי שהתנהגות התוכנה תהיה מיטבית.
לעיתים נקרא למקרה בדיקה “טסט”.

זה מתחיל ברמת אפיון המערכת כאשר שואלים שאלות על איך המערכת אמורה להתנהג ולהיראות.

למשל בבאג במערכת הפיננסית היינו יכולים לשאול - האם האסטרטגיה לקנייה ומכירה של מניות מניבה רווחים?
בעולם מתוקן היינו מצפים להרצה “יבשה” של המערכת על מנת לראות אם יש רווחים - לתהליך הזה אנחנו קוראים בדיקות.

“הרצה יבשה” אומר שאנחנו לא מכניסים נתונים אמיתיים או מריצים את הקוד בסביבה האמיתית שתגרום לנו לאיבוד כסף.
ורק לאחר תהליך נוקשה לאימות הקוד - נוכל לשחרר את הקוד לטבע.

בפסודו קוד היינו יכולים לנסח את זה באופן הבא:

1
2
3
4
5
6
7
strategy = NewStrategy()

strategy.Bid(Bidding(Stock = Microsoft ,Ask = 100))
strategy.Bid(Bidding(Stock = Apple ,Ask = 200))
strategy.Bid(Bidding(Stock = Netflix ,Ask = 300))

validate strategy.Profits > 0 # Assume we made profits in the previoud bidding.

כמובן שהדוגמא הזו היא סופר מפושטת על מנת להבין את התהליך.
וכמו שרואים זהו תהליך ששואלים בו שאלות - האם הרווחים אחרי הקנייה של המניות גדולים מ-0, זאת אומרת האם רווחנו פה כסף.

במקרה של המערכת הרפואית - לומר לזכותם היה מאוד קשה לעלות על באג כזה מכיוון שזהו גם כשל מכני של המערכת.
אך בשביל זה יש לוודא שכלל הרכיבים עובדים כמצופה בכל המצבים.
אנו קוראים למצבים כאלו מצבי קיצון.
אלו מקרי קצה שקשה לעלות עליהם ולכן גם קשה לבדוק אותם.
וככל הנראה אם אנשים לא היו מתים גם לא היו עולים על הבאג הזה!


תכנון בדיקות ואימות התנהגות

בדיקות תוכנה מתחלקות ע”פ היקף הרכיבים.

  1. היקף הרכיב הקטן - למשל פונקציה או מחלקה.
  2. בדיקות מערכתיות - בדיקה שרכיבים בתוך המערכת עובדים.
  3. בדיקות אינטגרציה - חיבור בין מערכות ושרתים, למשל איך ה-UI עובד ביחד עם המערכת.
  4. בדיקות מקיפות - בדיקות בתצורות שונות כדי למצוא מקרי קצה שונים.
  5. בדיקות A/B - בדיקות בתצורה מסוימת על מנת להשוות התנהגות.
  6. בדיקות ביצועים - בדיקות מכל סוג שהוא על מנת למדוד ביצועים ויעילות המערכות.

ככל שעולים בהיקף כך גם העלות והזמן שנשקיע בבדיקות גדלה.
גם ככל שנשקיע בבדיקות כך גם איכות התוכנה שלנו תעלה והסיכוי לבאגים ייקטן!

בפוסט הנ”ל נלמד איך לבצע בדיקות בהיקף 1 ו-2.
ז”א נלמד לכתוב בדיקות לרכיבים שלנו, ובדיקות למערכות שלנו.

בשביל לבצע בדיקות נכיר 2 מתודולוגיות שעוזרות לנו להציב מטרות:

  • TDD - Test driven design/development.
  • BDD - Behavior driven design/development.

TDD - Test Driven Design

לפני כל קוד שנכתוב יש לכתוב מקרה בדיקה.

למשל - המשימה שלנו היא לממש פונקציית חיבור.
במתודולוגיית ה-TDD נכתוב קודם כל מקרה בדיקה ולא מימוש החיבור.

1
2
3
4
5
6
7
8
9
10
def Add(a,b):
pass


def TestAdd(add):
result = add(2,3)
expected = 5
are(result,expected).equal() # עוד על זה בהמשך

TestAdd(Add)

הבדיקה תבדוק אם פונקציית החיבור תיתן לנו את התוצאה הרצויה.
שימו לב שערך ה-expected הוא התוצאה ולא פעולת חיבור בפני עצמה.

אין לכתוב:

1
2
3
4
def TestAdd(add):
result = add(2,3)
expected = 2 + 3 # this is bad! We're doing something the add function does.
are(result,expected).equal()

הסיבה לכך שאם נכתוב את אותו קוד שהפונקציה מבצעת - אז אנחנו לא באמת בודקים אותה אלה מחקים אותה.
במקום זה יש ליצור מידע שהוא יהיה התוצאה.
במקרים פשוטים יהיה קל לייצר את המידע הזה.
במקרה מסובכים יותר - זה יהיה מורכב יותר.

בשיטה הזו מצופה לכתוב מקרה בדיקה שנכשל.
ורק לאחר מימוש המתודה - נראה שהוא עובר.

1
2
3
4
def Add(a,b):
return a + b

# עכשיו TestAdd יהיה שמח

BDD - Behavior Driven Design

המתודולוגיה הזו נבנתה על גבי ה-TDD.
במקום מקרי בדיקה - ההתנהגות היא במרכז.
בצורה תיאורית ניתן לומר מה המערכת עושה וכיצד היא אמורה להתנהג.

בשיטה הזו נגדיר כמה פרמטרים כמקרי בדיקה:

  • מי מבצע
  • סיטואציות המדמות התנהגות

למשל עבור ניהול מחסן ספרים:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
נושא: הוספה של ספרים למחסן  
כמחסנאי שמוסיף מלאי לספרים
אני רוצה להוסיף ספרים חדשים למלאי
על מנת שהספרים יהיו בספירה של מערכת המלאי

סיטואציה 1-
בהנחה שאין לנו את הכותרת במלאי
כאשר מוסיפים ספר חדש למלאי
אז המערכת תיצור כותרת חדשה לספר ע"פ המידע עליו ותכניס אותו למלאי.

סיטואציה 2-
בהנחה שהספר הזה קיים כבר במלאי
כאשר מוסיפים ספר למלאי
אז המערכת תוסיף עתקים על הכותרת ותסכום את כמות העותקים במלאי.

שימו לב לתצורה כאן שניתן לחזור עליה:

1
2
3
4
5
6
7
8
9
10
11
כותרת
כ....
אני...
על מנת....

סיטואציה -
בהנחה ש....
ו....
כאשר...
אז...

בתצורה הזו אנחנו יכולים להגדיר במדויק התנהגות ללא קשר לפרטים הטכניים של המערכת.
אין לנו כאן ידע על המימוש - לא מסד הנתונים, לא התקשורת ולא המבנים.

עקרונות כתיבת טסטים

שמות תיאוריים

יש ליצור שמות תיאוריים שמדייקים במה שמתבצע או בתפקידם.

שמות טסטים בדרך כלל מתחילים ב-test_ אך בקונבנציה ניתן לא להוסיף את התחילית הזו.

שם הפונקצייה צריך להיות מדוייק, אני משתמש במודל:
test_function_parameters_results.

למשל:
test_div_onzero_throws

השם של הטסט חשוב כדי להבין מה הטסט עושה ואילו אילוצים קיימים שם.
כאשר מצטברים מקרי בדיקה הטסטר ייכתוב את שמותיהם וע”פ שמות אינדיקטיביים נוכל בדיוק להבין איפה הבאג.
למשל:

1
2
3
div_onzero_throws               PASS
div_nine_three_returns_three PASS
add_five_five_returns_ten PASS

Assign - Act - Assert

גוף הטסט בנוי מ-3 שלבים.

  1. יצירת ערכים ואתחולם
  2. קריאה לפונקציה
  3. בדיקה שהערכים תקינים
1
2
3
4
5
6
7
8
9
def test_do_returns_zero_on_success():
# Assign
myClass = MyClass()

# Act
retValue = myClass.Do()

# Assert
assert retValue == 0

כפילויות זה בסדר

אם יש לנו קוד משוכפל בין טסטים זה בסדר.
כל טסט מגדיר את תחום האחריות שלו.

לעיתים ניתן להמיר קוד משוכפל לפונקציה הניתנת לשימוש חוזר.
אך אם זה מורכב מדי - לא חייבים לבצע את זה.

קריאות

אחת מהמטרות של טסטים טובים זה לתעד את השימושיות במערכת.
כאשר מישהו לומד על המערכת הוא יירצה להבין איך המערכת מתנהגת וטסטים יכולים לתאר בצורה מיטבית את ההתנהגות.
ולכן - על הטסטים להיות קלים לקריאה והבנה.

סביבה מבודדת

טסטים צריכים להיות כמה שיותר מבודדים.
הרצה מבודדת של הבדיקות לא תשפיע על המערכת באופן כללי מה שייאפשר להריץ הרבה מקרי בדיקה בבת אחת ובמקביל.

הרצה חוזרת ומהירה

טסטים טובים הם מהירים וניתן להריץ אותם בכל זמן.
זה מאפשר להוכיח את התנהגות המערכת לפני ואחרי כל שינוי במערכת.


סיכום העקרונות:

  1. תתארו את הטסטים בשפה מדויקת.
  2. שיטת ה-AAA, Assign, Act, Assert.
  3. כפיליות זה בסדר
  4. הסביבה צריכה להיות מובדדת.
  5. טסטים צריכים להיות קריאים.
  6. ניתן לחזור על טסטים ולהריץ אותם במהירות.

unittest

אחת הסיבות לפופולריות של פייתון היא הקלות בה לכתוב בדיקות.
unittest זוהי ספרייה מובנית בפייתון לבדיקות.

Test Fixture

Fixture הוא איגוד של בדיקות המצריכות הכנה מוקדמת של סביבת הבדיקות וקוד ניקוי.
למשל אם הקוד שלנו מצריך תיקייה במחשב אז ה-Fixture יידאג ליצור את התיקייה ולנקות אותה בסוף ההרצה.

Test Case

מקרה בדיקה יחיד.

Test Suite

איגוד של מקרי בדיקה - Test Case או כמה איגודים - Test Suite.
יכול להיות אחד מהם או שניהם.

Test Runner

מנהל ומריץ את כלל הטסטים.

כתיבת Unit test

כדי לכתוב טסט יש לרשת את המחלקה unittest.TestCase.
ולאחר מכן להוסיף פונקציה עם התחילית test_.
שימו לב שב-pytest צריך להוסיף את התחילית.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import unittest

def Add(a, b):
return a + b

class MathTests(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass

def test_add(self):
a = 5
b = 6
expected = 11

result = Add(a,b)

self.assertEqual(result, expected)

if __name__ == '__main__':
unittest.main()

הרצה

ניתן להריץ את הקובץ באופן רגיל:
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
2
3
4
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

כאשר הוא לא עובר זה ידפיס:

1
2
3
4
5
6
7
8
9
10
11
12
13
F
======================================================================
FAIL: test_add (tests.MathTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\ASUS\AppData\Local\Temp\py\tests.py", line 20, in test_add
self.assertEqual(result, expected)
AssertionError: -1 != 11

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

assertEquals

יבדוק אם שני אובייקטים זהים.

assertTrue

יבדוק אם האובייקט הוא True.

assertFalse

יבדוק אם האובייקט הוא False.

assertRaises

יבדוק אם קטע הקוד זורק שגיאה.

1
2
3
4
5
def do():
throw MyError

with self.assertRaises(MyError):
do()

setUp ו-tearDown

ניתן לממש את הפונקציות setUp ו-tearDown על מנת לבצע איתחול וניקוי לכל test case.

הספרייה מחפשת פונקציות בצורה אוטומטית המתחילות עם המילה test_.
כך שהפונקציה test_add(self) תהיה פונקציה לבדיקה.

setUpClass ו-tearDownClass

הפונקציות האלו ירוצו לפני כל טסט ואחרי כל טסט.
על מנת ליצור setUp יחיד ניתן להשתמש בפונקציות setUpClass ו-tearDownClass.

1
2
3
4
5
6
7
8
class MathTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("set up class")

@classmethod
def tearDownClass(cls):
print("\ntear down")

setUpModule ו-tearDownModule

פונקציות אתחול וניקוי לכל ה-מודול.

מממשים אותם כפונקציות רגילות במודול:

1
2
3
4
5
6
7
8
9
def setUpModule():
print("Module Hi")


def tearDownModule():
print("Module bye")

if __name__ == '__main__':
unittest.main()

תרגול

כתבו טסטים לפעולות המתמטיות:

  1. חיסור
  2. כפל
  3. חילוק
  4. חזקה

שימו לב למקרי הבדיקה - יש לבדוק שחילוק ב-0 זורק!


Pytest

pytest היא ספרייה נוספת לא מובנית בפייתון אשר מתפארת בקלות שלה לשימוש.

לכתוב ולהריץ טסטים נעשת בצורה מאוד פשטנית, כמו שלמדנו על התחיליות גם כאן יש שימוש בהן:

1
2
3
4
5
6
7
8
import pytest

# content of test_sample.py
def inc(x):
return x + 1

def test_answer():
assert inc(4) == 5

מריצים בעזרת pytest:
pytest myTests.py.

בדיקת שגיאות

גם ל-pytest קיימת פונקציה לתפיסת שגיאות בטסטים:

1
2
3
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0

ניתן גם להשתמש בדקורטור:

1
2
3
4
5
6
7
8
import pytest

def raisesEx():
raise IndexError

@pytest.mark.xfail(raises=IndexError)
def test_raisesEx():
raisesEx()

היתרונות בספריה הזו:

  • גמישות בכתיבת קוד
  • משתמשת ב-assert של פייתון.
  • יכולה לבצע אינטגרציות לספריות אחרות.
  • מריצה טסטים גם של unittest.

היתרונות ב-unittest:

  • בנוי בתצורת OOP מה שמאפשר סדר יותר מוחלט.
  • מובנת ונתמכת ע”י פייתון.
  • assertים מובנים לספרייה.

העברת פרמטרים לטסטים

לעיתים נרצה לבצע טסט מספר פעמים עם גורמים שונים.

1
2
3
def test_Add_With(a,b, result):
res = a + b
assert res == result

ואז נוכל לבדוק:

1
2
3
test_Add_With(1,2,3)
test_Add_With(5,6,11)
test_Add_With(5,-5,0)

בעזרת הדקורטור Parametrized נוכל ליישם זאת:

1
2
3
4
5
6
import pytest


@pytest.mark.parametrize("a,b,result", [(1, 2, 3), (5, 6, 11), (5, -5, 0)])
def test_eval(a, b, result):
assert a + b == result

בפרמטר הראשון אנו מציינים איך הפרמטר בנוי - יש לנו 2 גורמים ותוצאה
ובפרמטר השני אנו מעבירים רשימה של טאפלים כפרמטר.
כל טאפל הוא טסט ייחודי משלו.


עקרונות לטסטביליות - Testability

לא מספיק רק לכתוב בדיקות לקוד שלנו - יש לבצע ניתוח פעיל על הקוד שלנו כדי לוודא שניתן לבדוק אותו בכלל!

אבסטרקציה

לעיתים קוד שמסתמך על פרטים ספציפיים לא ניתן לבדוק אותו.
כמו למשל ה-date.now().

הקוד הנ”ל יהיה קשה לבדיקה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from datetime import date
from dataclasses import dataclass
from typing import Callable, List
import pytest


class Worker:
def Work(self):
pass


@dataclass
class ScheduledTask:
Task: Worker
Date: date


class MyScheduler:
def __init__(self) -> None:
self.__tasks = []

def ScheduleToDay(self, runOnDay, task):
self.__tasks.append(ScheduledTask(Task=task, Date=runOnDay))

def RunScheduler(self):
today = date.today()

for task in self.__tasks:
if today == task.Date:
task.Task.Work()


class StubWorker(Worker):
def __init__(self):
self.Flag = False

def Work(self):
self.Flag = True


def test_MyScheduler_RunScheduler_Run_On_Specified_Day():
scheduler = MyScheduler()
today = date.today()
myStubWorker = StubWorker()

# Run in three days from now
scheduler.ScheduleToDay(
date(today.year, today.month, today.day + 3), myStubWorker)

scheduler.RunScheduler() # Worker will never run!

assert myStubWorker.Flag

כדי לאפשר לבדוק את הקוד יש צורך באבסטרקציה ל-date.today() על מנת לשלוט ביום שמוחזר.
ניתן בקלות ליצור מחלקה להתנהגות הזו:

1
2
3
4
5
6
7
8
9
10
class DateTime:
def Today(self):
return date.today()

class DateTimeStub(DateTime):
def __init__(self, dayToReturn):
self.__dayToReturn = dayToReturn

def Today(self):
return self.__dayToReturn

כעת ניתן להשתמש ב-DateTime משלנו:

1
2
3
4
5
6
7
8
9
10
11
class MyScheduler:
def __init__(self, dateTime) -> None:
self.__tasks = []
self.__dateTime = dateTime

def RunScheduler(self):
today = self.__dateTime.Today()

for task in self.__tasks:
if today == task.Date:
task.Task()

פיצול

לעיתים קוד שמאוגד לפונקציות ארוכות או פעולות רבות לא יכול להיבדק כראוי ויש הרבה תנאים כדי שנוכל לבדוק אותו.

למשל יש לנו פונקציה במחלקה אשר לוקחת נתונים ממקור חיצוני, מבצעת חישוב ומחזירה בהתאם לתנאי את כל המידע.

1
2
3
4
5
6
7
8
9
class DeriveData:
def __init__(self, dataProvider):
self.__dataProvider = dataProvider

def Get(self, func):
data = self.__dataProvider.Get('SELECT * FROM EMPLOYEES WHERE WORKING_YEARS > 1')
for single in data:
if func(single):
yield single

על מנת לבדוק את DeriveData.Get עם פונקציה אשר מחפשת על כל רשומה צריך מקור חיצוני כדי לעבוד איתו.
זה מפר את עקרון הסביבה המבודדת!

מה שצריך לעשות כדי לאפשר לבדוק את הפונקציה הזו היא לפצל את הקוד!

1
2
3
4
5
6
7
8
9
10
class DeriveData:
def __init__(self):
#self.__dataProvider = dataProvider

def Get(self, data, func):
# __dataProvider should be outside of this
# data = self.__dataProvider.get('SELECT * FROM EMPLOYEES WHERE WORKING_YEARS > 1')
for single in data:
if func(single):
yield single

עכשיו נוכל להזריק את המידע data מבחוץ ולאפשר לבדוק את היחידה הזו בפרטניות.

הזרקת תלויות מבחוץ

אם היחידה תלויה במידע או התנהגות חיצונית - נעדיף להזריק אותה מבחוץ מאשר בפנים.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DataProvider:
def Get(self, Name=''):
pass

class DeriveData:
def __init__(self):
self.__dataProvider = DataProvider()

def GetByName(self, name):
data = self.__dataProvider.get(Name= f'*{name}*')

# Fix name to upper
for person in data:
person.name = person.name.upper()

return data

הבעיה כאן היא השורות האלו:

1
2
def __init__(self):
self.__dataProvider = DataProvider()

במקום ליצור את המחלקה בפנים כדאי לקבל אותה מבחוץ בתצורה הזו:

1
2
def __init__(self, dataProvider):
self.__dataProvider = dataProvider

כך שנוכל להזריק התנהגות שונה.
תצורה זו נקראת Stub או Mock.
במקום להזריק התנהגות אמיתית מזריקים התנהגות מזויפת שמתנהגת כמו שאנחנו רוצים.

למשל אם נרצה להזריק DataProvider משלנו נוכל לרשת את המחלקה ולהחזיר משהו אחר:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest

class Data:
def __init__(self, name):
self.name = name

class DataProviderStub(DataProvider):
def __init__(self, data):
self.__data = data

def Get(self, Name=''):
return self.__data

def test_DeriveData_GetByName_Uppercases_Names():
dataStub = DataProviderStub([Data('bob'), Data('alice')])
deriveData = DeriveData(dataStub)

data = deriveData.GetByName('Bob')

assert data[0].name == 'BOB'
assert data[1].name == 'ALICE'

# data will be [1,2,3]

Stub הוא אובייקט פשוט אשר מטרתו להחזיר ערך שאנחנו מצפים לו.
Mock לעומתו הוא אובייקט יותר מורכב אשר אפשר לשלוט בהגדרות.
נלמד בפרק הבא יותר על מוקים וכיצד להגדיר אותם!


תרגיל

  1. ב-unittest, כיצד מגדירים אתחול וסיום אחרי כל טסט בודד?
  2. ב-unittest, כיצד מגדירים אתחול וסיום אחרי כל Fixture?
  3. ב-unittest, כיצד מגדירים אתחול וסיום אחרי כל מודול?
  4. יש לכתוב טסטים למחלקה הבאה:

ניתן להיעזר בפרק על json כדי להבין את ההתנהגות:

JSON פייתון 12.2 - קבצי
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import json

class FlagConfiguration:
def __init__(self,data = {}):
self.__data = data

def TurnOn(self, name):
self.__data[name] = True

def TurnOff(self, name):
self.__data[name] = False

def IsOn(self, name):
if name in self.__data:
return self.__data[name]

def ToJson(self):
return json.dumps(self.__data, indent=1)
  1. תתקנו את המחלקה כך שהטסט ירוץ:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from datetime import date
from dataclasses import dataclass
from typing import Callable, List
import pytest


class Worker:
def Work(self):
pass


@dataclass
class ScheduledTask:
Task: Worker
Date: date


class MyScheduler:
def __init__(self) -> None:
self.__tasks = []

def ScheduleToDay(self, runOnDay, task):
self.__tasks.append(ScheduledTask(Task=task, Date=runOnDay))

def RunScheduler(self):
today = date.today()

for task in self.__tasks:
if today == task.Date:
task.Task()


class StubWorker(Worker):
def __init__(self):
self.Flag = False

def Work(self):
self.Flag = True


def test_MyScheduler_RunScheduler_Run_On_Specified_Day():
scheduler = MyScheduler()
today = date.today()
myStubWorker = StubWorker()

# Run in three days from now
scheduler.ScheduleToDay(
date(today.year, today.month, today.day + 3), myStubWorker)

scheduler.RunScheduler() # Worker will never run!

assert myStubWorker.Flag


ההבדל בין מתכנת למהנדס תוכנה הוא היכולת לתכנן ולכתוב אפיונים.
חלק מתהליך העיצוב הוא גם תהליך הבדיקות - כיצד בכלל נבדוק שהתוכנה שלנו עובדת כראוי.
ללא ספק זהו תהליך מורכב וחשוב לא פחות מכתיבת מימוש המערכת ואם לכל מוצר בעולם יהיו בדיקות מקיפות, נספק ללקוחות ולעצמינו מערכות בטוחות, אמינות ויעילות!

פרק הבא:

פייתון 28 - בדיקות - זיוף התנהגות

אהבתם? מוזמנים להביע תמיכה כאן: כוס קפה