Имитация ftplib.FTP для модульного тестирования кода Python

Я не знаю, почему я просто не понимаю этого, но я хочу использовать mock в Python, чтобы проверить, что мои функции правильно вызывают функции в 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» находилась в том же файле, а в том, чтобы вызывать ее из отдельного файла модуля.


person Tamerz    schedule 23.04.2015    source источник


Ответы (3)


Когда вы делаете patch(ftplib.FTP), вы исправляете конструктор FTP. dowload_file() используйте его для создания объекта ftp, поэтому ваш объект ftp, для которого вы вызываете login() и cmd(), будет 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 только потому, что это хорошая практика

person Michele d'Amico    schedule 23.04.2015
comment
Это не совсем сработало для меня, но помог этот вопрос. Мне пришлось изменить строку mock_ftp = mock_ftp_constructor.return_value на mock_ftp = mock_ftp_constructor.return_value.__enter__.return_value. - person Tony; 02.05.2018
comment
Привет, я пробовал решение выше, но не работает для моего кода, я разместил здесь вопрос: stackoverflow.com/questions/66426586/, не могли бы вы взглянуть на меня, пожалуйста? - person Cecilia; 02.03.2021

Я предлагаю использовать pytest и pytest-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')
person Ibrohim Ermatov    schedule 27.12.2017

Как и ответ Иброхима, я предпочитаю pytest с мокером.

Я пошел немного дальше и на самом деле написал библиотеку, которая помогает мне легко издеваться. Вот как использовать его для вашего случая.

Вы начинаете с того, что у вас есть свой код и базовая функция 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 при использовании моей библиотеки, я принимаю запросы на включение.

person Peter K    schedule 07.08.2019