《Mock在Python單元測試中的使用》要點:
本文介紹了Mock在Python單元測試中的使用,希望對您有用。如果有疑問,可以聯系我們。
本文講述的是 Python 中 Mock 的使用.
如何執行單元測試而不用考驗你的耐心
很多時候,我們編寫的軟件會直接與那些被標記為“垃圾”的服務交互.用外行人的話說:服務對我們的應用程序很重要,但是我們想要的是交互,而不是那些不想要的副作用,這里的“不想要”是在自動化測試運行的語境中說的.例如:我們正在寫一個社交 app,并且想要測試一下 “發布到 Facebook” 的新功能,但是不想每次運行測試集的時候真的發布到 Facebook.
Python 的?unittest
?庫包含了一個名為?unittest.mock
?或者可以稱之為依賴的子包,簡稱為?mock
?—— 其提供了極其強大和有用的方法,通過它們可以模擬并去除那些我們不希望的副作用.
注意:mock最近被收錄[1]到了Python3.3的標準庫中;先前發布的版本必須通過PyPI[2]下載Mock庫.
再舉另一個例子,我們在接下來的部分都會用到它,這是就是系統調用.不難發現,這些系統調用都是主要的模擬對象:無論你是正在寫一個可以彈出 CD 驅動器的腳本,還是一個用來刪除 /tmp 下過期的緩存文件的 Web 服務,或者一個綁定到 TCP 端口的 socket 服務器,這些調用都是在你的單元測試上下文中不希望產生的副作用.
作為一個開發者,你需要更關心你的庫是否成功地調用了一個可以彈出 CD 的系統函數(使用了正確的參數等等),而不是切身經歷 CD 托盤每次在測試執行的時候都打開了.(或者更糟糕的是,彈出了很多次,在一個單元測試運行期間多個測試都引用了彈出代碼!)
同樣,保持單元測試的效率和性能意味著需要讓如此多的“緩慢代碼”遠離自動測試,比如文件系統和網絡訪問.
對于第一個例子來說,我們要從原始形式換成使用?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
?的引用(LCTT 譯注:注意?@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
?方法.這意味著我們可以檢查實例本身.如果你想要了解更多,可以試著在你模擬的代碼下斷點,以對這種修補機制的原理獲得更好的認識.
當我們在測試方法中使用多個裝飾器,其順序是很重要的,并且很容易混亂.基本上,當裝飾器被映射到方法參數時,裝飾器的工作順序是反向的[3].思考這個例子:
? ?@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 的工作方式[4]所導致的.這里是使用多個裝飾器的情況下它們執行順序的偽代碼:
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[5]?和?mock.MagicMock[6],大多數內部函數都是建立在這兩個類之上的.當在選擇使用?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,我們可以非常容易地動手寫一個更加智能的測試用例.
即使對它的使用還有點不太熟悉,對單元測試[7]來說,Python的mock庫可以說是一個規則改變者.我們已經演示了常見的用例來了解了mock在單元測試中的使用,希望這篇文章能夠幫助Python開發者[8]克服初期的障礙,寫出優秀、經受過考驗的代碼.
via:https://www.toptal.com/python/an-introduction-to-mocking-in-python
作者:NAFTULITZVIKAY[9]譯者:cposture[10]校對:wxy[11]
本文由LCTT[12]原創翻譯,Linux中國[13]榮譽推出
[1]: http://www.python.org/dev/peps/pep-0417/
[2]: https://pypi.python.org/pypi/mock
[3]: http://www.voidspace.org.uk/python/mock/patch.html#nesting-patch-decorators
[4]: http://docs.python.org/2/reference/compound_stmts.html#function-definitions
[5]: http://www.voidspace.org.uk/python/mock/mock.html
[6]: http://www.voidspace.org.uk/python/mock/magicmock.html#magic-mock
[7]: http://www.toptal.com/qa/how-to-write-testable-code-and-why-it-matters
[8]: http://www.toptal.com/python
[9]: http://www.slviki.com/
[10]: https://github.com/cposture
[11]: https://github.com/wxy
[12]: https://github.com/LCTT/TranslateProject
[13]: https://linux.cn/
推薦閱讀
python配置文件操作
python集合類型實例
初入運維的小伙伴,別再問需不需要學Python了
轉載請注明本頁網址:
http://www.snjht.com/jiaocheng/4438.html