Mocking os.path.exists and os.makedirs returning AssertionError

Question:

I have a function like below.

# in retrieve_data.py

import os

def create_output_csv_file_path_and_name(output_folder='outputs') -> str:
    """
    Creates an output folder in the project root if it doesn't already exist.
    Then returns the path and name of the output CSV file, which will be used
    to write the data.
    """
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)
        logging.info(f"New folder created for output file: " f"{output_folder}")

    return os.path.join(output_folder, 'results.csv')

I also created a unit test file like below.

# in test_retrieve_data.py

class OutputCSVFilePathAndNameCreationTest(unittest.TestCase):

    @patch('path.to.retrieve_data.os.path.exists')
    @patch('path.to.retrieve_data.os.makedirs')
    def test_create_output_csv_file_path_and_name_calls_exists_and_makedirs_once_when_output_folder_is_not_created_yet(
            self,
            os_path_exists_mock,
            os_makedirs_mock
    ):
        os_path_exists_mock.return_value = False
        retrieve_cradle_profile_details.create_output_csv_file_path_and_name()
        os_path_exists_mock.assert_called_once()
        os_makedirs_mock.assert_called_once()

But when I run the above unit test, I get the following error.

def assert_called_once(self):
    """assert that the mock was called only once.
    """
    if not self.call_count == 1:
        msg = ("Expected '%s' to have been called once. Called %s times.%s"
               % (self._mock_name or 'mock',
                  self.call_count,
                  self._calls_repr()))
         raise AssertionError(msg)
         AssertionError: Expected 'makedirs' to have been called once. Called 0 times.

I tried poking around with pdb.set_trace() in create_output_csv_file_path_and_name method and I’m sure it is receiving a mocked object for os.path.exists(), but the code never go pasts that os.path.exists(output_folder) check (output_folder was already created in the program folder but I do not use it for unit testing purpose and want to keep it alone). What could I possibly be doing wrong here to mock os.path.exists() and os.makedirs()? Thank you in advance for your answers!

Asked By: user1330974

||

Answers:

You have the arguments to your test function reversed. When you have stacked decorators, like:

@patch("retrieve_data.os.path.exists")
@patch("retrieve_data.os.makedirs")
def test_create_output_csv_file_path_...():

They apply bottom to top, so you need to write:

@patch("retrieve_data.os.path.exists")
@patch("retrieve_data.os.makedirs")
def test_create_output_csv_file_path_and_name_calls_exists_and_makedirs_once_when_output_folder_is_not_created_yet(
    self, os_makedirs_mock, os_path_exists_mock
):

With this change, if I have this in retrieve_data.py:

import os
import logging

def create_output_csv_file_path_and_name(output_folder='outputs') -> str:
    """
    Creates an output folder in the project root if it doesn't already exist.
    Then returns the path and name of the output CSV file, which will be used
    to write the data.
    """
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)
        logging.info(f"New folder created for output file: " f"{output_folder}")

    return os.path.join(output_folder, 'results.csv')

And this is test_retrieve_data.py:

import unittest
from unittest.mock import patch

import retrieve_data


class OutputCSVFilePathAndNameCreationTest(unittest.TestCase):
    @patch("retrieve_data.os.path.exists")
    @patch("retrieve_data.os.makedirs")
    def test_create_output_csv_file_path_and_name_calls_exists_and_makedirs_once_when_output_folder_is_not_created_yet(
        self, os_makedirs_mock, os_path_exists_mock
    ):
        os_path_exists_mock.return_value = False
        retrieve_data.create_output_csv_file_path_and_name()

        os_path_exists_mock.assert_called_once()
        os_makedirs_mock.assert_called_once()

Then the tests run successfully:

$ python -m unittest -v
test_create_output_csv_file_path_and_name_calls_exists_and_makedirs_once_when_output_folder_is_not_created_yet (test_retrieve_data.OutputCSVFilePathAndNameCreationTest.test_create_output_csv_file_path_and_name_calls_exists_and_makedirs_once_when_output_folder_is_not_created_yet) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

Update I wanted to leave a comment on the diagnostics I performed here, because I didn’t initially spot the reversed arguments, either, but the problem became immediately apparent when I added a breakpoint() the beginning of the test and printed out the values of the mocks:

(Pdb) p os_path_exists_mock
<MagicMock name='makedirs' id='140113966613456'>
(Pdb) p os_makedirs_mock
<MagicMock name='exists' id='140113966621072'>

The fact that the names were swapped made the underlying problem easy to spot.

Answered By: larsks