Search code examples
pythonunit-testingpython-mock

Mocking ftplib.FTP for unit testing Python code


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.


Solution

  • 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