【问题标题】:Mocking ftplib.FTP for unit testing Python code模拟 ftplib.FTP 以对 Python 代码进行单元测试
【发布时间】:2015-07-01 07:33:39
【问题描述】:

我不知道为什么我没有得到这个,但我想在 Python 中使用 mock 来测试我的函数是否正确调用了 ftplib.FTP 中的函数。我已经简化了一切,但仍然没有弄清楚它是如何工作的。这是一个简单的例子:

import unittest
import ftplib
from unittest.mock import patch

def download_file(hostname, file_path, file_name):
    ftp = ftplib.FTP(hostname)
    ftp.login()
    ftp.cwd(file_path)

class TestDownloader(unittest.TestCase):

    @patch('ftplib.FTP')
    def test_download_file(self, mock_ftp):
        download_file('ftp.server.local', 'pub/files', 'wanted_file.txt')

        mock_ftp.cwd.assert_called_with('pub/files')

当我运行它时,我得到:

AssertionError: Expected call: cwd('pub/files')
Not called

我知道它一定是在使用模拟对象,因为那是一个虚假的服务器名称,并且在不打补丁的情况下运行时,它会抛出“socket.gaierror”异常。

如何获取函数正在运行的实际对象?长期目标是不在同一个文件中包含“download_file”函数,而是从单独的模块文件中调用它。

【问题讨论】:

    标签: python unit-testing python-mock


    【解决方案1】:

    当您执行patch(ftplib.FTP) 时,您正在修补FTP 构造函数。 dowload_file() 使用它构建 ftp 对象,因此您调用login()cmd()ftp 对象将是mock_ftp.return_value 而不是mock_ftp

    您的测试代码应如下所示:

    class TestDownloader(unittest.TestCase):
    
        @patch('ftplib.FTP', autospec=True)
        def test_download_file(self, mock_ftp_constructor):
            mock_ftp = mock_ftp_constructor.return_value
            download_file('ftp.server.local', 'pub/files', 'wanted_file.txt')
            mock_ftp_constructor.assert_called_with('ftp.server.local')
            self.assertTrue(mock_ftp.login.called)
            mock_ftp.cwd.assert_called_with('pub/files')
    

    我添加了所有检查和autospec=True 只是因为是good practice

    【讨论】:

    • 这对我来说不太奏效,但this question 帮了大忙。我不得不将行 mock_ftp = mock_ftp_constructor.return_value 更改为 mock_ftp = mock_ftp_constructor.return_value.__enter__.return_value
    • 您好,我尝试了上面的解决方案,但不适用于我的代码,我在这里发布了一个问题:stackoverflow.com/questions/66426586/…,您能帮我看看吗?
    【解决方案2】:

    我建议使用 pytestpytest-mock

    from pytest_mock import mocker
    
    
    def test_download_file(mocker):
        ftp_constructor_mock = mocker.patch('ftplib.FTP')
        ftp_mock = ftp_constructor_mock.return_value
    
        download_file('ftp.server.local', 'pub/files', 'wanted_file.txt')
    
        ftp_constructor_mock.assert_called_with('ftp.server.local')
        assert ftp_mock.login.called
        ftp_mock.cwd.assert_called_with('pub/files')
    

    【讨论】:

      【解决方案3】:

      Ibrohim's answer,我更喜欢pytestmocker

      我已经走得更远了,实际上有wrote a library 这有助于我轻松地模拟。以下是如何在您的情况下使用它。

      您首先拥有您的代码和一个基本的 pytest 函数,然后添加我的帮助程序库来生成模块的模拟和匹配的断言生成:

      import ftplib
      
      from mock_autogen.pytest_mocker import PytestMocker
      
      
      def download_file(hostname, file_path, file_name):
          ftp = ftplib.FTP(hostname)
          ftp.login()
          ftp.cwd(file_path)
      
      
      def test_download_file(mocker):
          import sys
          print(PytestMocker(mocked=sys.modules[__name__],
                             name=__name__).mock_modules().prepare_asserts_calls().generate())
          download_file('ftp.server.local', 'pub/files', 'wanted_file.txt')
      

      当你第一次运行测试时,它会因为未知的 DNS 而失败,但是包装我的库的打印语句会给我们这个有价值的输入:

      ...
      mock_ftplib = mocker.MagicMock(name='ftplib')
      mocker.patch('test_29817963.ftplib', new=mock_ftplib)
      ...
      import mock_autogen
      ...
      print(mock_autogen.generator.generate_asserts(mock_ftplib, name='mock_ftplib'))
      

      我将其放入测试中并再次运行:

      def test_download_file(mocker):
          mock_ftplib = mocker.MagicMock(name='ftplib')
          mocker.patch('test_29817963.ftplib', new=mock_ftplib)
      
          download_file('ftp.server.local', 'pub/files', 'wanted_file.txt')
      
          import mock_autogen
          print(mock_autogen.generator.generate_asserts(mock_ftplib, name='mock_ftplib'))
      

      这次测试成功了,我只需要收集第二次打印的结果就可以得到正确的断言:

      def test_download_file(mocker):
          mock_ftplib = mocker.MagicMock(name='ftplib')
          mocker.patch(__name__ + '.ftplib', new=mock_ftplib)
      
          download_file('ftp.server.local', 'pub/files', 'wanted_file.txt')
      
          mock_ftplib.FTP.assert_called_once_with('ftp.server.local')
          mock_ftplib.FTP.return_value.login.assert_called_once_with()
          mock_ftplib.FTP.return_value.cwd.assert_called_once_with('pub/files')
      

      如果您想在使用我的库时继续使用unittest,我是accepting pull requests

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-06
        • 2017-03-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多