1. 程式人生 > 實用技巧 >python mock實踐筆記

python mock實踐筆記

前言

如果你寫程式碼時會寫單元測試(unit test,UT),那麼多半會遇到想要將某個函式隔離開,去掉外部依賴的情況,例如這個函式依賴其他函式的返回或者依賴某個API呼叫的返回。這種情況下就一定繞不開mock這項技術。本文並不打算介紹python下mock的方方面面,只會寫我個人實際使用中覺得比較實用的部分。

mocking是什麼?

mock就是模擬的意思,mocking主要用在單元測試中,當被測試的物件依賴另一個複雜的物件時,我們需要模擬這個複雜的物件的行為,mocking就是建立這個物件,模擬它的行為。

基本用法

unittest.mock模組包含了mock相關的功能。

Mock物件

看如下一段程式碼:

from unittest.mock import Mock
# 建立一個mock物件
mock = Mock()
print(mock)
# 結果:<Mock id='140668914327224'> 表明此處的mock是一個Mock物件

Mock非常靈活,當我們訪問一個Mock物件的某個屬性時,這個屬性如果不存在會被自動建立,看如下程式碼:

# 在訪問之前mock並沒有some_attribute這個屬性
print(mock.some_attribute)
# 結果:<Mock name='mock.some_attribute' id='140348173848360'> 
# 可見,在訪問的時候建立了該屬性

print(mock.do_something)
# 結果:<Mock name='mock.do_something' id='140348173886128'>

正是由於此特性,Mock可以用來模擬任意物件。

下面從最基本的開始介紹:

設定返回值和屬性

Mock物件可以返回常量,也可以隨著輸入返回不同值。

返回常量-return_value

mock = Mock()
# 設定返回值
mock.return_value = 3
print(mock()) # 返回 3
# 設定方法的返回值
mock.method.return_value = 3
mock.method() # 返回 3
# 在建構函式中設定返回值
mock = Mock(return_value=3)
mock() # 返回 3
# 設定屬性
mock = Mock()
mock.x = 3
mock.x # 返回 3

返回隨著輸入變化-side_effect

side_effect也算一個屬性,當你不滿足指定一個常量返回時,就會期望用上它。

# 1.將side_effect設定為一個異常類
mock = Mock(side_effect=Exception('Boom!'))
mock() # 呼叫時就會丟擲異常

from requests.exceptions import Timeout
requests = Mock()
requests.get.side_effect = Timeout # 模擬API超時
with self.assertRaises(Timeout):
    # get_holidays函式裡面呼叫了requests.get,那麼將會捕獲到Timeout異常
    get_holidays()
# 將會丟擲異常

# 2.將side_effect設定為一個迭代器(場景:mock物件被多次呼叫,每次返回值不一樣)
mock = MagicMock(side_effect=[4, 5, 6])
mock()
4
mock()
5
mock()
6

# 3.將side_effect設定為一個函式(場景:返回值由輸入引數決定)
vals = {(1, 2): 1, (2, 3): 2}
def side_effect(*args):
    return vals[args]

mock = MagicMock(side_effect=side_effect)
mock(1, 2)
1
mock(2, 3)
2

mock一個類

def some_function():
    instance = module.Foo()
    return instance.method()
# 模擬Foo這個類
with patch('module.Foo') as mock:
    # 此處的“mock”就是一個類,mock.return_value代表該類返回的例項(instance)
    instance = mock.return_value
    # 模擬例項方法的返回(此處的方法名就叫method)
    instance.method.return_value = 'the result'
    # 函式中對Foo的呼叫就會使用模擬類
    result = some_function()
    assert result == 'the result'

模擬一個物件(object)的方法(method)

patch

patch可能是使用最多的方法,它的使用場景:

1.模擬一個類的屬性

2.模擬一個模組的屬性

如果我們測試的函式在同一個檔案中可以不使用patch,patch主要用在測試程式碼和主程式碼分離的情況下。

有3種裝飾器可用:

# patch的第一個引數是一個string,形式:package.module.Class.attribute,以此指定要模擬的屬性,第二個引數是可選的,第一個引數裡面的屬性將被替換為該值。
@patch('package.module.attribute', sentinel.attribute)
# 例子一,傳有第二個引數:
mock = MagicMock(return_value=sentinel.file_handle)
with patch('builtins.open', mock):
    handle = open('filename', 'r')
# 例子二,不傳第二個引數:
# 不傳第二個引數時,mock物件會被傳入在函式的引數裡,如下,並且注意順序:
class MyTest(unittest.TestCase):
    @patch('package.module.ClassName1')
    @patch('package.module.ClassName2')
    def test_something(self, MockClass2, MockClass1):
        self.assertIs(package.module.ClassName1, MockClass1)
        self.assertIs(package.module.ClassName2, MockClass2)
# 例子三,使用as,將會獲得一個引用
with patch('ProductionClass.method') as mock_method:
    mock_method.return_value = None
    real = ProductionClass()
    real.method(1, 2, 3)

# 例子四,將path裝飾在類上,作用於每個測試函式
@patch('mymodule.SomeClass')
class MyTest(unittest.TestCase):

    def test_one(self, MockSomeClass):
        self.assertIs(mymodule.SomeClass, MockSomeClass)

    def test_two(self, MockSomeClass):
        self.assertIs(mymodule.SomeClass, MockSomeClass)
		# 裝飾在類上只針對test開頭的函式,此處不是test開頭,不傳遞MockSomeClass引數
    def not_a_test(self):
        return 'something'

# 例子五,另一種在整個類中模擬的辦法
class MyTest(unittest.TestCase):
    def setUp(self):
        patcher = patch('mymodule.foo')
        self.addCleanup(patcher.stop)
        self.mock_foo = patcher.start()

    def test_foo(self):
        self.assertIs(mymodule.foo, self.mock_foo)


# patch的類在同一個檔案中
# 此處需要使用__main__,代表當前模組
@patch('__main__.SomeClass')


# patch.object第一個引數是一個物件,第二個引數是該物件的屬性名稱,第三個是可選的,第二個引數裡面的屬性將被替換為該值。
# 場景:只模擬部分屬性而非整個物件
@patch.object(SomeClass, 'attribute', sentinel.attribute)

@patch.dict()

如下為例子,供參考、copy:

# my_calendar.py
import requests
from datetime import datetime

def is_weekday():
    today = datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return (0 <= today.weekday() < 5)

def get_holidays():
    r = requests.get('http://localhost/api/holidays')
    if r.status_code == 200:
        return r.json()
    return None

# tests.py
import unittest
from my_calendar import get_holidays
from requests.exceptions import Timeout
from unittest.mock import patch

class TestCalendar(unittest.TestCase):
    # patch裝飾在函式上如果函式裡面會呼叫my_calendar下的requests函式,就會被mock掉
    @patch('my_calendar.requests')
    def test_get_holidays_timeout(self, mock_requests):
            mock_requests.get.side_effect = Timeout
            with self.assertRaises(Timeout):
                get_holidays()
                mock_requests.get.assert_called_once()

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

mock可以直接使用裝飾器,也可以使用上下文管理器,為什麼使用上下文管理器?一般原因有如下兩個,可自行判斷要不要用:

1.只想針對部分程式碼,而不是整個測試函式

2.patch裝飾器已經很多了,裝飾器太多影響可讀性

patch路徑應該是什麼?

where to patch?並不是要去引用某個函式本身所在的位置,而是要看這個函式在哪裡使用的,如果在使用了的地方有import,那麼就應該是那個地方的路徑。
舉例:

一個檔案中(這個檔案路徑:package2.m2.py):

from package1.m1 import fun1
def fun2():
  fun1()

在另一個測試檔案中:

class JustTest(TestCase):
  @patch('package2.m2.fun1') # 這才是正確的路徑,而不是package1.m1.fun1
  def test_fun2(self, mock_fun1):
    mock_fun1.return_value = 3

參考

python官方示例

How to: Unit testing in Django with mocking and patching

What is Mocking?

https://realpython.com/python-mock-library/