Mocking ftplib.FTP for unit testing Python code
Asked Answered
N

3

14

I don't know why I'm just not getting this, but I want to use mock in Python to test that my functions are calling functions in ftplib.FTP correctly. I've simplified everything down and still am not wrapping my head around how it works. Here is a simple example:

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

When I run this, I get:

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

I know it must be using the mock object since that is a fake server name, and when run without patching, it throws a "socket.gaierror" exception.

How do I get the actual object the fuction is running? The long term goal is not having the "download_file" function in the same file, but calling it from a separate module file.

Nalepka answered 23/4, 2015 at 8:36 Comment(0)
A
13

When you do patch(ftplib.FTP) you are patching FTP constructor. dowload_file() use it to build ftp object so your ftp object on which you call login() and cmd() will be mock_ftp.return_value instead of mock_ftp.

Your test code should be follow:

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

I added all checks and autospec=True just because is a good practice

Anemometer answered 23/4, 2015 at 9:54 Comment(2)
This didn't quite work for me, but this question helped. I had to change the line mock_ftp = mock_ftp_constructor.return_value to mock_ftp = mock_ftp_constructor.return_value.__enter__.return_value.Humdinger
Hi I tried the solution above but don't work for my code, I posted a question here: #66427086, could you take a look for me please?Kopple
O
2

Like Ibrohim's answer, I prefer pytest with mocker.

I have went a bit further and have actually wrote a library which helps me to mock easily. Here is how to use it for your case.

You start by having your code and a basic pytest function, with the addition of my helper library to generate mocks to modules and the matching asserts generation:

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

When you run the test for the first time, it would fail due to unknown DNS, but the print statement which wraps my library would give us this valuable input:

...
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'))

I'm placing this in the test and would run it again:

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

This time the test succeeds and I only need to collect the result of the second print to get the proper asserts:

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

If you would like to keep using unittest while using my library, I'm accepting pull requests.

Okhotsk answered 7/8, 2019 at 14:26 Comment(0)
W
1

I suggest using pytest and 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')
Wartime answered 27/12, 2017 at 7:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.