• בלוג
  • מדריך מקוצר לפקודה patch ב Python

מדריך מקוצר לפקודה patch ב Python

04/05/2019

ספריית unittest של Python כוללת Decorator בשם patch שתפקידו לעזור לנו לבדוק קוד שיש לו תלויות שקשה לבדוק. במדריך זה נראה מהן תלויות שקשה לבדוק וכמה דוגמאות לשימוש בפקודה patch כדי להתגבר על קושי זה.

1. קוד שקשה לבדוק בגלל תלויות

הפונקציה הבאה מגרילה 7 מספרים באקראי ומחשבת את סכומם:

def sum_seven_random_numbers():
    return sum([random.randint(1, 100) for _ in range(7)])

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

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

במקרה של הפונקציה שלנו נגיד שהיא תלויה בפונקציה החיצונית random.randint.

הפונקציה patch של פייתון מאפשרת להחליף לפונקציות שלנו את מימושי התלויות שלו, בלי לגעת בקוד המערכת. כך נוכל ״להחליף״ באופן זמני את המימוש של random.randint. הפונקציה שלנו תקרא למימוש החדש והמזויף שכתבנו וכך נוכל לבדוק שהפונקציה sum_seven_random_numbers באמת מפעילה את random.randint 7 פעמים וסוכמת את התוצאות.

2. דוגמא 1 - שינוי תלות כך שתחזיר ערך קבוע

בואו נכתוב בדיקה לפונקציה באמצעות patch. הפקודה patch היא Decorator ולכן מופעלת באמצעות סימן @ מעל פונקציית הבדיקה. היא מקבלת כפרמטר ראשון את שם הפונקציה אותה רוצים להחליף ואחריו באמצעות Keyword Arguments את אפשרויות ההחלפה.

הקוד הבא מדגים תוכנית בדיקה באמצעות patch שתגרום ל random.randint להחזיר תמיד את הערך הקבוע 10, ולכן סכום של 7 מספרים אקראיים יהיה תמיד 70:

import random
import unittest
from unittest.mock import patch

def sum_seven_random_numbers():
    return sum([random.randint(1, 100) for _ in range(7)])

class TestDemo(unittest.TestCase):
    @patch('random.randint', return_value=10)
    def test_sum(self, fake_randint):
        self.assertEqual(70, sum_seven_random_numbers())

unittest.main()

3. דוגמא 2 - שינוי תלות כך שתחזיר ערך מרשימה

לפעמים אנחנו רוצים להחזיר ערך שונה בכל הפעלה של התלות, לדוגמא בשביל לדמות שחקן מחשב במשחק מסוים יכול להיות שנרצה שהערכים שיוחזרו מ random.randint באמת יהיו שונים כל פעם (אבל עדיין צפויים כדי שנוכל לבדוק את הקוד שמתבסס עליהם).

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

הדוגמא הבאה תגרום לפונקציה שלנו להאמין שהיא קיבלה את 7 המספרים האקראיים 1, 2, 3, 4, 5, 6, 7 ולכן סכומם יהיה 28:

import random
import unittest
from unittest.mock import patch

def sum_seven_random_numbers():
    return sum([random.randint(1, 100) for _ in range(7)])

class TestDemo(unittest.TestCase):
    random_results = [1, 2, 3, 4, 5, 6, 7]

    @patch('random.randint', side_effect=random_results)
    def test_sum(self, fake_randint):
        self.assertEqual(28, sum_seven_random_numbers())

unittest.main()

4. דוגמא 3 - וידוא שימוש בתלות מסוימת

לפעמים מועיל לבדוק שהפונקציה בה אנו תלויים באמת נקראה ועם הפרמטרים הנכונים. במקרה שלנו אכן בדקנו שהפונקציה sum_seven_random_numbers לוקחת 7 מספרים מ random.randint וסוכמת אותם, אבל לא בדקנו שהיא מעבירה את הפרמטרים הנכונים ל random.randint. המימוש המזויף שלנו התעלם מהפרמטרים שעברו אליו.

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

import random
import unittest
from unittest.mock import patch

def sum_seven_random_numbers():
    return sum([random.randint(1, 100) for _ in range(7)])

class TestDemo(unittest.TestCase):
    random_results = [1, 2, 3, 4, 5, 6, 7]

    @patch('random.randint', side_effect=random_results)
    def test_sum(self, fake_randint):
        self.assertEqual(28, sum_seven_random_numbers())
        fake_randint.assert_called_with(1, 100)

unittest.main()

הפקודה assert_called_with מוודאת שבכל הקריאות לפונקציה random.randint הפרמטרים שעברו היו 1 ו-100. שני מאפיינים מועילים נוספים של אוביקט הפונקציה המזויפת הם call_count שמחזיר כמה פעמים קראו לה ו calls שמחזיר מערך של הקריאות כך שתוכלו לבדוק לגבי כל קריאה ספציפית איזה פרמטרים היא קיבלה.