如何在Python单元测试中模拟文件系统?

Dud*_*ock 39 python filesystems unit-testing mocking

有没有一种标准的方法(没有安装第三方库)来在Python中进行跨平台文件系统模拟?如果我必须使用第三方库,哪个库是标准的?

Nil*_*rth 30

pyfakefs(主页)做你想要的 - 一个虚假的文件系统; 它是第三方,但该派对是谷歌.有关使用的讨论,请参阅如何替换待测模块的文件访问引用.

对于嘲笑,unittest.mock是用于Python 3.3+(标准库PEP 0417); 对于早期版本,请参阅PyPI:mock(适用于Python 2.5+)(主页).

测试和嘲笑中的术语不一致; 使用Gerard Meszaros 的Test Double术语,你要求的是"假的":行为类似于文件系统(你可以创建,打开和删除文件),但不是实际的文件系统(在这种情况下它是在内存中),因此您不需要拥有测试文件或临时目录.

在经典的模拟中,你会模拟系统调用(在Python中,模拟os模块中的函数,比如os.rmos.listdir),但这更加繁琐.


jtm*_*lia 12

Python 3.3+中的标准模拟框架是unittest.mock ; 你可以将它用于文件系统或其他任何东西.

您也可以通过猴子修补模拟来简单地手动滚动它:

一个简单的例子:

import os.path
os.path.isfile = lambda path: path == '/path/to/testfile'
Run Code Online (Sandbox Code Playgroud)

更完整(未经测试):

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())                 
Run Code Online (Sandbox Code Playgroud)

在这个例子中,实际的模拟是微不足道的,但你可以使用具有状态的东西来支持它们,这样可以表示文件系统操作,例如保存和删除.是的,这有点难看,因为它需要在代码中复制/模拟基本文件系统.

请注意,你不能猴子补丁python内置.话虽如此...

对于早期版本,如果可能的话使用第三方库,我会选择Michael Foord的真棒Mock,unittest.mock由于PEP 0417,它现在已经在3.3+以上的标准库中了,你可以在PyPI for Python 2.5+ 上获得它.而且,它可以模拟内置的!


Den*_*nis 9

pytest正在获得很大的吸引力,它可以使用tmpdirmonkeypatching(mocking)完成所有这些工作.

您可以使用tmpdirfunction参数,该参数将提供在基本临时目录(默认情况下创建为系统临时目录的子目录)中创建的测试调用唯一的临时目录.

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
Run Code Online (Sandbox Code Playgroud)

monkeypatch函数的参数可以帮助您安全设置/删除属性,字典项或环境变量或修改sys.path导入.

import os
def test_some_interaction(monkeypatch):
    monkeypatch.setattr(os, "getcwd", lambda: "/")
Run Code Online (Sandbox Code Playgroud)

您也可以将其传递给函数而不是使用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')
Run Code Online (Sandbox Code Playgroud)

如果你的应用程序与文件系统有很多交互,那么使用像pyfakefs这样的东西可能更容易,因为模拟会变得乏味和重复.


web*_*rc2 8

假装还是嘲弄?

就个人而言,我发现文件系统中有很多边缘情况(比如用正确的权限打开文件,字符串vs二进制,读/写模式等),并且使用准确的假文件系统可以找到很多你可能通过嘲弄找不到的错误.在这种情况下,我会查看memoryfs模块pyfilesystem(它具有相同接口的各种具体实现,因此您可以在代码中交换它们).

模拟(没有猴子补丁!):

也就是说,如果你真的想要模拟,你可以使用Python的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!')
Run Code Online (Sandbox Code Playgroud)

上面的示例仅演示了通过open()模拟方法创建和写入文件,但您可以轻松地模拟任何方法.