简体   繁体   English

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

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

Is there a standard way (without installing third party libraries) to do cross platform filesystem mocking in Python?是否有标准方法(无需安装第三方库)在 Python 中进行跨平台文件系统模拟? If I have to go with a third party library, which library is the standard?如果我必须使用第三方库,哪个库是标准的?

pyfakefs ( homepage ) does what you want – a fake filesystem; pyfakefs ( homepage ) 做你想做的事——一个文件系统; it's third-party, though that party is Google.它是第三方,尽管该方是谷歌。 See How to replace file-access references for a module under test for discussion of use.有关使用的讨论,请参阅如何替换被测模块的文件访问引用

For mocking , unittest.mock is the standard library for Python 3.3+ ( PEP 0417 );对于模拟, unittest.mock是 Python 3.3+ ( PEP 0417 ) 的标准库; for earlier version see PyPI: mock (for Python 2.5+) ( homepage ).对于早期版本,请参阅PyPI: mock (for Python 2.5+) ( homepage )。

Terminology in testing and mocking is inconsistent;测试和模拟中的术语不一致; using the Test Double terminology of Gerard Meszaros, you're asking for a “fake”: something that behaves like a filesystem (you can create, open, and delete files), but isn't the actual file system (in this case it's in-memory), so you don't need to have test files or a temporary directory.使用 Gerard Meszaros 的Test Double术语,您要求的是“假”:行为类似于文件系统的东西(您可以创建、打开和删除文件),但不是实际的文件系统(在这种情况下它是内存中),因此您不需要测试文件或临时目录。

In classic mocking, you would instead mock out the system calls (in Python, mock out functions in the os module, like os.rm and os.listdir ), but that's much more fiddly.在经典模拟中,您将改为模拟系统调用(在 Python 中,模拟os模块中的函数,如os.rmos.listdir ),但这要复杂得多。

pytest is gaining a lot of traction, and it can do all of this using tmpdir and monkeypatching (mocking). pytest获得了很大的吸引力,它可以使用tmpdirmonkeypatching (模拟)来完成所有这些工作。

You can use the tmpdir function argument which will provide a temporary directory unique to the test invocation, created in the base temporary directory (which are by default created as sub-directories of the system temporary directory).您可以使用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

The monkeypatch function argument helps you to safely set/delete an attribute, dictionary item or environment variable or to modify sys.path for importing. monkeypatch函数参数可帮助您安全地设置/删除属性、字典项或环境变量,或修改sys.path以进行导入。

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

You can also pass it a function instead of using 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')

If your application has a lot of interaction with the file system, then it might be easier to use something like pyfakefs , as mocking would become tedious and repetitive.如果您的应用程序与文件系统有很多交互,那么使用pyfakefs 之类的东西可能会更容易,因为模拟会变得乏味和重复。

The standard mocking framework in Python 3.3+ is unittest.mock ; Python 3.3+ 中的标准模拟框架是unittest.mock you can use this for the filesystem or anything else.您可以将其用于文件系统或其他任何东西。

You could also simply hand roll it by mocking via monkey patching:您也可以通过猴子补丁模拟来简单地手动滚动它:

A trivial example:一个简单的例子:

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

A bit more full (untested):更完整一点(未经测试):

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())                 

In this example, the actual mocks are trivial, but you could back them with something that has state so that can represent filesystem actions, such as save and delete.在这个例子中,实际的模拟是微不足道的,但你可以用一些有状态的东西来支持它们,这样就可以代表文件系统操作,比如保存和删除。 Yes, this is all a bit ugly since it entails replicating/simulating basic filesystem in code.是的,这有点难看,因为它需要在代码中复制/模拟基本文件系统。

Note that you can't monkey patch python builtins.请注意,您不能猴子修补 python 内置函数。 That being said...话虽如此...

For earlier versions, if at all possible use a third party library, I'd go with Michael Foord's awesome Mock , which is now unittest.mock in the standard library since 3.3+ thanks to PEP 0417 , and you can get it on PyPI for Python 2.5+.对于早期版本,如果可能使用第三方库,我会选择 Michael Foord 的真棒Mock ,由于PEP 0417 ,它现在是标准库中的unittest.mock 3.3+,你可以在PyPI上获得它Python 2.5+。 And, it can mock builtins!而且,它可以模拟内置函数!

Faking or Mocking?伪装还是嘲讽?

Personally, I find that there are a lot of edge cases in filesystem things (like opening the file with the right permissions, string-vs-binary, read/write mode, etc), and using an accurate fake filesystem can find a lot of bugs that you might not find by mocking.就我个人而言,我发现文件系统中存在很多边缘情况(比如以正确的权限打开文件、字符串与二进制、读/写模式等),使用准确的假文件系统可以找到很多您可能无法通过模拟找到的错误。 In this case, I would check out the memoryfs module of pyfilesystem (it has various concrete implementations of the same interface, so you can swap them out in your code).在这种情况下,我会检查pyfilesystemmemoryfs模块(它具有相同接口的各种具体实现,因此您可以在代码中将它们换掉)。

Mocking (and without Monkey Patching!):模拟(并且没有猴子补丁!):

That said, if you really want to mock, you can do that easily with Python's unittest.mock library:也就是说,如果你真的想模拟,你可以使用 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!')

The above example only demonstrates creating and writing to files via mocking the open() method, but you could just as easily mock any method.上面的示例仅演示了通过模拟open()方法创建和写入文件,但您可以轻松模拟任何方法。

The standard unittest.mock library has a mock_open() function which provides basic mocking of the file system.标准的unittest.mock库有一个mock_open()函数,它提供文件系统的基本模拟。

Benefits: It's part of the standard library, and inherits the various features of Mocks, including checking call parameters & usage.优点:它是标准库的一部分,继承了 Mocks 的各种功能,包括检查调用参数和用法。

Drawbacks: It doesn't maintain filesystem state the way pytest or pyfakefs or mockfs does, so it's harder to test functions that do R/W interactions or interact with multiple files simultaneously.缺点:它不像pytestpyfakefsmockfs那样维护文件系统状态,因此更难测试执行 R/W 交互或同时与多个文件交互的函数。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM