目錄html
本文講述的是 Python 中 Mock 的使用python
如何在避免測試你的耐心的狀況下執行單元測試api
不少時候,咱們編寫的軟件會直接與那些被標記爲骯髒無比的服務交互。用外行人的話說:交互已設計好的服務對咱們的應用程序很重要,可是這會給咱們帶來不但願的反作用,也就是那些在一個自動化測試運行的上下文中不但願的功能。緩存
例如:咱們正在寫一個社交 app,而且想要測試一下 "發佈到 Facebook" 的新功能,可是不想每次運行測試集的時候真的發佈到 Facebook。服務器
Python 的 unittest
庫包含了一個名爲 unittest.mock
或者能夠稱之爲依賴的子包,簡稱爲
mock
—— 其提供了極其強大和有用的方法,經過它們能夠模擬和打樁來去除咱們不但願的反作用。網絡
注意:
mock
最近收錄到了 Python 3.3 的標準庫中;先前發佈的版本必須經過 PyPI 下載 Mock 庫。app
再舉另外一個例子,思考一個咱們會在余文討論的系統調用。不難發現,這些系統調用都是主要的模擬對象:不管你是正在寫一個能夠彈出 CD 驅動的腳本,仍是一個用來刪除 /tmp 下過時的緩存文件的 Web 服務,或者一個綁定到 TCP 端口的 socket 服務器,這些調用都是在你的單元測試上下文中不但願的反作用。socket
做爲一個開發者,你須要更關心你的庫是否成功地調用了一個能夠彈出 CD 的系統函數,而不是切身經歷 CD 托盤每次在測試執行的時候都打開了。函數
做爲一個開發者,你須要更關心你的庫是否成功地調用了一個能夠彈出 CD 的系統函數(使用了正確的參數等等),而不是切身經歷 CD 托盤每次在測試執行的時候都打開了。(或者更糟糕的是,不少次,在一個單元測試運行期間多個測試都引用了彈出代碼!)post
一樣,保持單元測試的效率和性能意味着須要讓如此多的 "緩慢代碼" 遠離自動測試,好比文件系統和網絡訪問。
對於首個例子,咱們要從原始形式到使用 mock
重構一個標準 Python 測試用例。咱們會演示如何使用 mock 寫一個測試用例,使咱們的測試更加智能、快速,並展現更多關於咱們軟件的工做原理。
有時,咱們都須要從文件系統中刪除文件,所以,讓咱們在 Python 中寫一個可使咱們的腳本更加輕易完成此功能的函數。
#!/usr/bin/env python # -*- coding: utf-8 -*- import os def rm(filename): os.remove(filename)
很明顯,咱們的 rm
方法此時沒法提供比 os.remove
方法更多的相關功能,但咱們能夠在這裏添加更多的功能,使咱們的基礎代碼逐步改善。
讓咱們寫一個傳統的測試用例,即,沒有使用 mock
:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import os.path import tempfile import unittest class RmTestCase(unittest.TestCase): tmpfilepath = os.path.join(tempfile.gettempdir(), "tmp-testfile") def setUp(self): with open(self.tmpfilepath, "wb") as f: f.write("Delete me!") def test_rm(self): # remove the file rm(self.tmpfilepath) # test that it was actually removed self.assertFalse(os.path.isfile(self.tmpfilepath), "Failed to remove the file.")
咱們的測試用例至關簡單,可是在它每次運行的時候,它都會建立一個臨時文件而且隨後刪除。此外,咱們沒有辦法測試咱們的 rm
方法是否正確地將咱們的參數向下傳遞給 os.remove
調用。咱們能夠基於以上的測試認爲它作到了,但還有不少須要改進的地方。
讓咱們使用 mock 重構咱們的測試用例:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os') def test_rm(self, mock_os): rm("any path") # test that rm called os.remove with the right parameters mock_os.remove.assert_called_with("any path")
使用這些重構,咱們從根本上改變了測試用例的操做方式。如今,咱們有一個能夠用於驗證其餘功能的內部對象。
第一件須要注意的事情就是,咱們使用了 mock.patch
方法裝飾器,用於模擬位於 mymodule.os
的對象,而且將 mock 注入到咱們的測試用例方法。那麼只是模擬 os
自己,而不是 mymodule.os
下 os
的引用(注意 @mock.patch('mymodule.os')
即是模擬 mymodule.os
下的 os
,譯者注),會不會更有意義呢?
固然,當涉及到導入和管理模塊,Python 的用法很是靈活。在運行時,mymodule
模塊擁有被導入到本模塊局部做用域的 os
。所以,若是咱們模擬 os
,咱們是看不到 mock 在 mymodule
模塊中的做用的。
這句話須要深入地記住:
模擬測試一個項目,只須要了解它用在哪裏,而不是它從哪裏來。
若是你須要爲 myproject.app.MyElaborateClass
模擬 tempfile
模塊,你可能須要將 mock 用於 myproject.app.tempfile
,而其餘模塊保持本身的導入。
先將那個陷阱置身事外,讓咱們繼續模擬。
以前定義的 rm 方法至關的簡單。在盲目地刪除以前,咱們傾向於驗證一個路徑是否存在,並驗證其是不是一個文件。讓咱們重構 rm 使其變得更加智能:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path def rm(filename): if os.path.isfile(filename): os.remove(filename)
很好。如今,讓咱們調整測試用例來保持測試的覆蓋率。
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # set up the mock mock_path.isfile.return_value = False rm("any path") # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.") # make the file 'exist' mock_path.isfile.return_value = True rm("any path") mock_os.remove.assert_called_with("any path")
咱們的測試用例徹底改變了。如今咱們能夠在沒有任何反作用下覈實並驗證方法的內部功能。
到目前爲止,咱們只是將 mock 應用在函數上,並沒應用在須要傳遞參數的對象和實例的方法。咱們如今開始涵蓋對象的方法。
首先,咱們將 rm
方法重構成一個服務類。實際上將這樣一個簡單的函數轉換成一個對象,在本質上這不是一個合理的需求,但它可以幫助咱們瞭解 mock
的關鍵概念。讓咱們開始重構:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path class RemovalService(object): """A service for removing objects from the filesystem.""" def rm(filename): if os.path.isfile(filename): os.remove(filename)
你會注意到咱們的測試用例沒有太大變化:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm("any path") # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.") # make the file 'exist' mock_path.isfile.return_value = True reference.rm("any path") mock_os.remove.assert_called_with("any path")
很好,咱們知道 RemovalService
會如期工做。接下來讓咱們建立另外一個服務,將 RemovalService
聲明爲它的一個依賴:
:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path class RemovalService(object): """A service for removing objects from the filesystem.""" def rm(self, filename): if os.path.isfile(filename): os.remove(filename) class UploadService(object): def __init__(self, removal_service): self.removal_service = removal_service def upload_complete(self, filename): self.removal_service.rm(filename)
由於咱們的測試覆蓋了 RemovalService
,所以咱們不會對咱們測試用例中 UploadService
的內部函數 rm
進行驗證。相反,咱們將調用 UploadService
的 RemovalService.rm
方法來進行簡單測試(固然沒有其餘反作用),咱們經過以前的測試用例便能知道它能夠正確地工做。
這裏有兩種方法來實現測試:
由於這兩種方法都是單元測試中很是重要的方法,因此咱們將同時對這兩種方法進行回顧。
mock
庫有一個特殊的方法裝飾器,能夠模擬對象實例的方法和屬性,即 @mock.patch.object decorator
裝飾器:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService, UploadService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm("any path") # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.") # make the file 'exist' mock_path.isfile.return_value = True reference.rm("any path") mock_os.remove.assert_called_with("any path") class UploadServiceTestCase(unittest.TestCase): @mock.patch.object(RemovalService, 'rm') def test_upload_complete(self, mock_rm): # build our dependencies removal_service = RemovalService() reference = UploadService(removal_service) # call upload_complete, which should, in turn, call `rm`: reference.upload_complete("my uploaded file") # check that it called the rm method of any RemovalService mock_rm.assert_called_with("my uploaded file") # check that it called the rm method of _our_ removal_service removal_service.rm.assert_called_with("my uploaded file")
很是棒!咱們驗證了 UploadService 成功調用了咱們實例的 rm 方法。你是否注意到一些有趣的地方?這種修補機制(patching mechanism)實際上替換了咱們測試用例中的全部 RemovalService
實例的 rm
方法。這意味着咱們能夠檢查實例自己。若是你想要了解更多,能夠試着在你模擬的代碼下斷點,以對這種修補機制的原理得到更好的認識。
當咱們在測試方法中使用多個裝飾器,其順序是很重要的,而且很容易混亂。基本上,當裝飾器被映射到方法參數時,裝飾器的工做順序是反向的。思考這個例子:
@mock.patch('mymodule.sys') @mock.patch('mymodule.os') @mock.patch('mymodule.os.path') def test_something(self, mock_os_path, mock_os, mock_sys): pass
注意到咱們的參數和裝飾器的順序是反向匹配了嗎?這多多少少是由 Python 的工做方式 致使的。這裏是使用多個裝飾器的狀況下它們執行順序的僞代碼:
patch_sys(patch_os(patch_os_path(test_something)))
由於 sys 補丁位於最外層,因此它最晚執行,使得它成爲實際測試方法參數的最後一個參數。請特別注意這一點,而且在運行你的測試用例時,使用調試器來保證正確的參數以正確的順序注入。
咱們可使用構造函數爲 UploadService 提供一個 Mock 實例,而不是模擬特定的實例方法。我更推薦方法 1,由於它更加精確,但在多數狀況,方法 2 或許更加有效和必要。讓咱們再次重構測試用例:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService, UploadService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm("any path") # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.") # make the file 'exist' mock_path.isfile.return_value = True reference.rm("any path") mock_os.remove.assert_called_with("any path") class UploadServiceTestCase(unittest.TestCase): def test_upload_complete(self, mock_rm): # build our dependencies mock_removal_service = mock.create_autospec(RemovalService) reference = UploadService(mock_removal_service) # call upload_complete, which should, in turn, call `rm`: reference.upload_complete("my uploaded file") # test that it called the rm method mock_removal_service.rm.assert_called_with("my uploaded file")
在這個例子中,咱們甚至不須要補充任何功能,只需爲 RemovalService
類建立一個 auto-spec,而後將實例注入到咱們的 UploadService
以驗證功能。
mock.create_autospec
方法爲類提供了一個同等功能實例。實際上來講,這意味着在使用返回的實例進行交互的時候,若是使用了非法的方式將會引起異常。更具體地說,若是一個方法被調用時的參數數目不正確,將引起一個異常。這對於重構來講是很是重要。當一個庫發生變化的時候,中斷測試正是所指望的。若是不使用 auto-spec,儘管底層的實現已經被破壞,咱們的測試仍然會經過。
mock
庫包含了兩個重要的類 mock.Mock 和 mock.MagicMock,大多數內部函數都是創建在這兩個類之上的。當在選擇使用 mock.Mock
實例,mock.MagicMock
實例或 auto-spec 的時候,一般傾向於選擇使用 auto-spec,由於對於將來的變化,它更能保持測試的健全。這是由於 mock.Mock
和 mock.MagicMock
會無視底層的 API,接受全部的方法調用和屬性賦值。好比下面這個用例:
class Target(object): def apply(value): return value def method(target, value): return target.apply(value)
咱們能夠像下面這樣使用 mock.Mock 實例進行測試:
class MethodTestCase(unittest.TestCase): def test_method(self): target = mock.Mock() method(target, "value") target.apply.assert_called_with("value")
這個邏輯看似合理,但若是咱們修改 Target.apply
方法接受更多參數:
class Target(object): def apply(value, are_you_sure): if are_you_sure: return value else: return None
從新運行你的測試,你會發現它仍能經過。這是由於它不是針對你的 API 建立的。這就是爲何你老是應該使用 create_autospec
方法,而且在使用 @patch
和 @patch.object
裝飾方法時使用 autospec
參數。
爲了完成,咱們寫一個更加適用的現實例子,一個在介紹中說起的功能:發佈消息到 Facebook。我將寫一個不錯的包裝類及其對應的測試用例。
import facebook class SimpleFacebook(object): def __init__(self, oauth_token): self.graph = facebook.GraphAPI(oauth_token) def post_message(self, message): """Posts a message to the Facebook wall.""" self.graph.put_object("me", "feed", message=message)
這是咱們的測試用例,它能夠檢查咱們發佈的消息,而不是真正地發佈消息:
import facebook import simple_facebook import mock import unittest class SimpleFacebookTestCase(unittest.TestCase): @mock.patch.object(facebook.GraphAPI, 'put_object', autospec=True) def test_post_message(self, mock_put_object): sf = simple_facebook.SimpleFacebook("fake oauth token") sf.post_message("Hello World!") # verify mock_put_object.assert_called_with(message="Hello World!")
正如咱們所看到的,在 Python 中,經過 mock,咱們能夠很是容易地動手寫一個更加智能的測試用例。
對 單元測試 來講,Python 的 mock
庫能夠說是一個遊戲變革者,即便對於它的使用還有點困惑。咱們已經演示了單元測試中常見的用例以開始使用 mock
,並但願這篇文章可以幫助 Python 開發者 克服初期的障礙,寫出優秀、經受過考驗的代碼。
via: https://www.toptal.com/python/an-introduction-to-mocking-in-python