簡體   English   中英

如何在 Python 單元測試中模擬文件系統?

[英]How do I mock the filesystem in Python unit tests?

是否有標准方法(無需安裝第三方庫)在 Python 中進行跨平台文件系統模擬? 如果我必須使用第三方庫,哪個庫是標准的?

pyfakefs ( homepage ) 做你想做的事——一個文件系統; 它是第三方,盡管該方是谷歌。 有關使用的討論,請參閱如何替換被測模塊的文件訪問引用

對於模擬, unittest.mock是 Python 3.3+ ( PEP 0417 ) 的標准庫; 對於早期版本,請參閱PyPI: mock (for Python 2.5+) ( homepage )。

測試和模擬中的術語不一致; 使用 Gerard Meszaros 的Test Double術語,您要求的是“假”:行為類似於文件系統的東西(您可以創建、打開和刪除文件),但不是實際的文件系統(在這種情況下它是內存中),因此您不需要測試文件或臨時目錄。

在經典模擬中,您將改為模擬系統調用(在 Python 中,模擬os模塊中的函數,如os.rmos.listdir ),但這要復雜得多。

pytest獲得了很大的吸引力,它可以使用tmpdirmonkeypatching (模擬)來完成所有這些工作。

您可以使用tmpdir函數參數,該參數將為測試調用提供一個唯一的臨時目錄,該目錄在基本臨時目錄中創建(默認情況下創建為系統臨時目錄的子目錄)。

import os
def test_create_file(tmpdir):
    p = tmpdir.mkdir("sub").join("hello.txt")
    p.write("content")
    assert p.read() == "content"
    assert len(tmpdir.listdir()) == 1

monkeypatch函數參數可幫助您安全地設置/刪除屬性、字典項或環境變量,或修改sys.path以進行導入。

import os
def test_some_interaction(monkeypatch):
    monkeypatch.setattr(os, "getcwd", lambda: "/")

你也可以給它傳遞一個函數而不是使用 lambda。

import os.path
def getssh(): # pseudo application code
    return os.path.join(os.path.expanduser("~admin"), '.ssh')

def test_mytest(monkeypatch):
    def mockreturn(path):
        return '/abc'
    monkeypatch.setattr(os.path, 'expanduser', mockreturn)
    x = getssh()
    assert x == '/abc/.ssh'

# You can still use lambda when passing arguments, e.g.
# monkeypatch.setattr(os.path, 'expanduser', lambda x: '/abc')

如果您的應用程序與文件系統有很多交互,那么使用pyfakefs 之類的東西可能會更容易,因為模擬會變得乏味和重復。

Python 3.3+ 中的標准模擬框架是unittest.mock 您可以將其用於文件系統或其他任何東西。

您也可以通過猴子補丁模擬來簡單地手動滾動它:

一個簡單的例子:

import os.path
os.path.isfile = lambda path: path == '/path/to/testfile'

更完整一點(未經測試):

import classtobetested                                                                                                                                                                                      
import unittest                                                                                                                                                                                             

import contextlib                                                                                                                                                                                           

@contextlib.contextmanager                                                                                                                                                                                  
def monkey_patch(module, fn_name, patch):                                                                                                                                                                   
    unpatch = getattr(module, fn_name)                                                                                                                                                                      
    setattr(module, fn_name)                                                                                                                                                                                
    try:                                                                                                                                                                                                    
        yield                                                                                                                                                                                               
    finally:                                                                                                                                                                                                
        setattr(module, fn_name, unpatch)                                                                                                                                                                   


class TestTheClassToBeTested(unittest.TestCase):                                                                                                                                                              
    def test_with_fs_mocks(self):                                                                                                                                                                           
        with monkey_patch(classtobetested.os.path,                                                                                                                                                          
                          'isfile',                                                                                                                                                                         
                          lambda path: path == '/path/to/file'):                                                                                                                                            
            self.assertTrue(classtobetested.testable())                 

在這個例子中,實際的模擬是微不足道的,但你可以用一些有狀態的東西來支持它們,這樣就可以代表文件系統操作,比如保存和刪除。 是的,這有點難看,因為它需要在代碼中復制/模擬基本文件系統。

請注意,您不能猴子修補 python 內置函數。 話雖如此...

對於早期版本,如果可能使用第三方庫,我會選擇 Michael Foord 的真棒Mock ,由於PEP 0417 ,它現在是標准庫中的unittest.mock 3.3+,你可以在PyPI上獲得它Python 2.5+。 而且,它可以模擬內置函數!

偽裝還是嘲諷?

就我個人而言,我發現文件系統中存在很多邊緣情況(比如以正確的權限打開文件、字符串與二進制、讀/寫模式等),使用准確的假文件系統可以找到很多您可能無法通過模擬找到的錯誤。 在這種情況下,我會檢查pyfilesystemmemoryfs模塊(它具有相同接口的各種具體實現,因此您可以在代碼中將它們換掉)。

模擬(並且沒有猴子補丁!):

也就是說,如果你真的想模擬,你可以使用 Python 的unittest.mock庫輕松地做到這一點:

import unittest.mock 

# production code file; note the default parameter
def make_hello_world(path, open_func=open):
    with open_func(path, 'w+') as f:
        f.write('hello, world!')

# test code file
def test_make_hello_world():
    file_mock = unittest.mock.Mock(write=unittest.mock.Mock())
    open_mock = unittest.mock.Mock(return_value=file_mock)

    # When `make_hello_world()` is called
    make_hello_world('/hello/world.txt', open_func=open_mock)

    # Then expect the file was opened and written-to properly
    open_mock.assert_called_once_with('/hello/world.txt', 'w+')
    file_mock.write.assert_called_once_with('hello, world!')

上面的示例僅演示了通過模擬open()方法創建和寫入文件,但您可以輕松模擬任何方法。

標准的unittest.mock庫有一個mock_open()函數,它提供文件系統的基本模擬。

優點:它是標准庫的一部分,繼承了 Mocks 的各種功能,包括檢查調用參數和用法。

缺點:它不像pytestpyfakefsmockfs那樣維護文件系統狀態,因此更難測試執行 R/W 交互或同時與多個文件交互的函數。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM