Mocking ftplib.FTP for unit testing Python code

I don’t know why I just don’t get this, but I want to use mock in Python to verify that my functions call functions correctly in ftplib.FTP. I simplified everything and still do not hug around me 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 that he should use a mock object, since this is the name of a fake server, and when launched without fixing it throws a socket.gaierror exception.

How to get the actual object that works fuction? The long-term goal is not that the download_file function is in the same file, but calls it from a separate module file.

+9
source share
3 answers

When you execute patch(ftplib.FTP) , you are fixing the FTP constructor. dowload_file() use it to create an FTP object, so your FTP object that you call login() and cmd() on will be mock_ftp.return_value instead of mock_ftp .

Your test code should look like this:

 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 it is good practice

+9
source

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') 
+1
source

Like ibrohim's answer , I prefer pytest with a mockery .

I went a little further and actually wrote a library that helps me mock easily. Here's how to use it in your case.

You start by having your own code and the basic pytest function, with the addition of my helper library for generating mock for modules and generating corresponding statements:

 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 will fail due to an unknown DNS, but the print statement that surrounds my library will give us such a valuable contribution:

 ... 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 put this in a test and will run 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 was successful, and I only need to collect the result of the second fingerprint to get the correct statements:

 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 want to continue using unittest while using my library, I accept fetch requests .

0
source

Source: https://habr.com/ru/post/985761/


All Articles