摘要:接下來我們將介紹如何對對象的方法進行模擬測試。選項創建模擬測試接口我們可以在的構造函數中提供一個模擬測試實例,而不是模擬創建具體的模擬測試方法。
如何不靠耐心測試
通常,我們編寫的軟件會直接與那些我們稱之為“骯臟的”服務交互。通俗地說,服務對我們的應用來說是至關重要的,它們之間的交互是我們設計好的,但這會帶來我們不希望的副作用——就是那些在我們自己測試的時候不希望的功能。
比如,可能我們正在寫一個社交軟件并且想測試一下“發布到Facebook的功能”,但是我們不希望每次運行測試集的時候都發布到Facebook上。
Python的unittest庫中有一個子包叫unittest.mock——或者你把它聲明成一個依賴,簡化為mock——這個模塊提供了非常強大并且有用的方法,通過它們可以模擬或者屏敝掉這些不受我們希望的方面。
注意:mock是最近收錄在Python 3.3標準庫中的;之前發布的版本必須通過 PyPI下載Mock庫。
再舉一個例子,考慮系統調用,我們將在余下的文章中討論它們。不難發現,這些都可以考慮使用模擬:無論你是想寫一個腳本彈出一個CD驅動,或者是一個web服務用來刪除/tmp目錄下的緩存文件,或者是一個socket服務來綁定一個TCP端口,這些調用都是在你單元測試的時候是不被希望的方面。
作為一個開發人員,你更關心你的庫是不是成功的調用了系統函數來彈出CD,而不是體驗每次測試的時候CD托盤都打開。
作為一個開發人員,你更關心你的庫是不是成功調用了系統函數來彈出CD(帶著正確的參數等)。而不是體驗每次測試的時候CD托盤都打開(或者更糟,很多次,當一個單元測試運行的時候,很多測試點都涉及到了彈出代碼)。
同樣地,保持你的單元測試效率和性能意味著要還要保留一些自動化測試之外的“緩慢代碼”,比如文件系統和網絡的訪問。
對于我們的第一個例子,我們要重構一個從原始到使用mock的一個標準Python測試用例。我們將會證明如何用mock寫一個測試用例使我們的測試更智能、更快,并且能暴露更多關于我們的軟件工作的問題。
有時,我們需要從文件系統中刪除文件,因此,我們可以寫這樣的一個函數在Python中,這個函數將使它更容易成為我們的腳本去完成這件事情。
#!/usr/bin/env python # -*- coding: utf-8 -*- import os def rm(filename): os.remove(filename)
很明顯,在這個時間點上,我們的rm方法不提供比基本os.remove方法更多的功能,但我們的代碼將會有所改進,允許我們在這里添加更多的功能。
讓我們寫一個傳統的測試用例,即,不用模擬測試:
#!/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")
對于這些重構,我們已經從根本上改變了該測試的運行方式?,F在,我們有一個內部的對象,讓我們可以使用另一個功能驗證。
潛在的陷阱第一件要注意的事情就是,我們使用的mock.patch方法的裝飾位于mymodule.os模擬對象,并注入到我們測試案例的模擬方法。是模擬os更有意義,還是它在mymodule.os的參考更有意義?
當然,當Python出現在進口和管理模塊時,用法是非常的靈活。在運行時,該mymodule模塊有自己的os操作系統——被引入到自己的范圍內的模塊。因此,如果我們模擬os系統,我們不會看到模擬測試在mymodule模塊的影響。
這句話需要深刻的記住:
模擬測試一個項目,只需要了解它用在哪里,而不是它從哪里來.
如果你需要為myproject.app.MyElaborateClass模擬tempfile模型,你可能需要去模擬myproject.app.tempfile的每個模塊來保持自己的進口。
這就是用陷阱的方式來模擬測試。
之前定義的 rm 方法相當的簡單 . 在盲目的刪除之前,我們會拿它來驗證一個路徑是否存在,并驗證其是否是一個文件. 讓我們重構 rm 使其變得更加聰明:
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport os.pathdef rm(filename): if os.path.isfile(filename): os.remove(filename)
很好. 現在,讓我們調整我們的測試用例來保持測試的覆蓋程度.
#!/usr/bin/env python# -*- coding: utf-8 -*-from mymodule import rmimport mockimport unittestclass 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")
我們的測試范例完全變化了. 現在我們可以核實并驗證方法的內部功能是否有任何副作用.
將刪除功能作為服務到目前為止,我們只是對函數功能提供模擬測試,并沒對需要傳遞參數的對象和實例的方法進行模擬測試。接下來我們將介紹如何對對象的方法進行模擬測試。
首先,我們先將rm方法重構成一個服務類。實際上將這樣一個簡單的函數轉換成一個對象并不需要做太多的調整,但它能夠幫助我們了解mock的關鍵概念。下面是重構的代碼:
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport os.pathclass 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 RemovalServiceimport mockimport unittestclass 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如同我們計劃的一樣工作。接下來讓我們創建另一個以該對象為依賴項的服務:
#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport os.pathclass RemovalService(object): """A service for removing objects from the filesystem.""" def rm(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(filename): self.removal_service.rm(filename)
到目前為止,我們的測試已經覆蓋了RemovalService, 我們不會對我們測試用例中UploadService的內部函數rm進行驗證。相反,我們將調用UploadService的RemovalService.rm方法來進行簡單的測試(為了不產生其他副作用),我們通過之前的測試用例可以知道它可以正確地工作。
有兩種方法可以實現以上需求:
模擬RemovalService.rm方法本身。
在UploadService類的構造函數中提供一個模擬實例。
因為這兩種方法都是單元測試中非常重要的方法,所以我們將同時對這兩種方法進行回顧。
該模擬庫有一個特殊的方法用來裝飾模擬對象實例的方法和參數。@mock.patch.object 進行裝飾:
#!/usr/bin/env python# -*- coding: utf-8 -*-from mymodule import RemovalService, UploadServiceimport mockimport unittestclass 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")
太棒了!我們驗證了上傳服務成功調用了實例的rm方法。你是不是注意到這當中有意思的地方了?這種修補機制實際上取代了我們的測試方法的刪除服務實例的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的工作方式。下面是使用多個裝飾方法的時候,實際的代碼執行順序:
view sourceprint?
patch_sys(patch_os(patch_os_path(test_something)))
由于這個關于sys的補丁在最外層,因此會在最后被執行,使得它成為實際測試方法的最后一個參數。請特別注意這一點,并且在做測試使用調試器來保證正確的參數按照正確的順序被注入。
我們可以在UploadService的構造函數中提供一個模擬測試實例,而不是模擬創建具體的模擬測試方法。 我推薦使用選項1的方法,因為它更精確,但在多數情況下,選項2是必要的并且更加有效。讓我們再次重構我們的測試實例:
#!/usr/bin/env python# -*- coding: utf-8 -*-from mymodule import RemovalService, UploadServiceimport mockimport unittestclass 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")
在這個例子中,我們甚至不需要補充任何功能,只需創建一個帶auto-spec方法的RemovalService類,然后將該實例注入到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 valuedef 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 API 調用在結束之際,讓我寫一個更加實用的真實世界的例子, 這在我們的介紹部分曾今提到過: 向Facebook發送一個消息. 我們會寫一個漂亮的封裝類,和一個產生回應的測試用例。
import facebookclass 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)
下面是我們的測試用例, 它檢查到我發送了信息,但并沒有實際的發送出這條信息(到Facebook上):
import facebookimport simple_facebookimport mockimport unittestclass 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 克服一開始的障礙,寫出優秀的,能經得起測試的代碼.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/8682.html
摘要:三性能測試工具官網介紹騰訊開源的的隨身調測平臺,支持和。官網介紹騰訊游戲部門開發的移動全平臺性能測試分析工具平臺。百度的服務目前主要為收費服務。 隨著移動互聯網的高速發展,App 應用非常火,測試工程師也會接觸到各種 app 應用。除了人工測試之外,也可以通過一些測試工具來提高我們的測試效率...
摘要:微信知乎新浪等主流網站的模擬登陸爬取方法摘要微信知乎新浪等主流網站的模擬登陸爬取方法。先說說很難爬的知乎,假如我們想爬取知乎主頁的內容,就必須要先登陸才能爬,不然看不到這個界面。圖片描述知乎需要手機號才能注冊登陸。 微信、知乎、新浪等主流網站的模擬登陸爬取方法摘要:微信、知乎、新浪等主流網站的模擬登陸爬取方法。 網絡上有形形色色的網站,不同類型的網站爬蟲策略不同,難易程度也不一樣。從是...
DevEco Studio特性介紹 1 開發平臺介紹1.1 應用開發1.2 設備開發 2 分布式應用開發2.1 UI設計Preview2.2 調試 X 往期文章 1 開發平臺介紹 鴻蒙開發包含了分布式應用開發和分布式設備開發 1.1 應用開發 鴻蒙應用是在多設備運行的,同一個工程可以在多設備自適應,根據設備選擇交互方式、UI布局,這個問題還在不斷的發展和完善,需要開發者共同推進 鴻蒙應用...
摘要:關于本教程有任何建議或者疑問,都歡迎郵件與我聯系,或者在上提出教程流程簡介教程將會從如何分析微信協議開始,第一部分將教你如何從零開始獲取并模擬擴展個人微信號所需要的協議。 現在的日常生活已經離不開微信,難免會生出微信有沒有什么API可以使用的想法。 那樣就可以拿自己微信做個消息聚合、開個投票什么的,可以顯然沒有這種東西。 不過還好,有網頁版微信不就等于有了API么,這個項目就是出于這個...
閱讀 3421·2021-10-20 13:49
閱讀 2793·2021-09-29 09:34
閱讀 3691·2021-09-01 11:29
閱讀 3081·2019-08-30 11:01
閱讀 838·2019-08-29 17:10
閱讀 866·2019-08-29 12:48
閱讀 2777·2019-08-29 12:40
閱讀 1348·2019-08-29 12:30