Python unittest to create a mock .json file

Question:

I have function that looks like this:

def file1_exists(directory):
    file1_path = os.path.join(directory, 'file1.json')
    return os.path.exists(file1_path)


def file2_exists(directory):
    log_path = os.path.join(directory, 'file2.log')
    return os.path.exists(file2_path)


def create_file1(directory):
    if file1_exists(directory):
        return

    if not file2_exists(directory):
        return

    mod_time = os.stat(os.path.join(directory, 'file2.log')).st_mtime
    timestamp = {
        "creation_timestamp": datetime.datetime.fromtimestamp(mod_time).isoformat()
    }

    with open(os.path.join(directory, "file1.json"), "w") as f:
        json.dump(timestamp, f)

And I need to create a unittest that uses mock files.

The 3 Unittests that I need are:

  1. A mock myfile.json file where I will assert that the function will return None (based on the 1st if statement, since the file exists)
  2. A way to mock-hide the data.txt item in order to assert that the function will return None (based on the second if statement)
  3. A mock myfile.json file where I write the required data and then assert that the return matches the expected outcome.

So far I’ve tried tests 1. and 2. with variations of this but I’ve been unsuccessful:

class TestAdminJsonCreation(unittest.TestCase):                                 
    @patch('os.path.exists', return_value=True)                                                    
    def test_existing_admin_json(self):                                         
        self.assertNone(postprocess_results.create_json_file())  

I’ve also read about other solutions such as:
Python testing: using a fake file with mock & io.StringIO
But I haven’t found a way to successfully do what I need…

Asked By: Oluevaera

||

Answers:

You want to be able to provide different return values for each call to os.path.exists. Since you know the order of the calls, you can use side_effects to supply a list of values to be used in order.

class TestAdminJsonCreation(unittest.TestCase): 

    # No JSON file                                
    @patch('os.path.exists', return_value=True)                                                    
    def test_existing_admin_json(self):                                         
        self.assertNone(postprocess_results.create_json_file())

    # JSON file, log file                                
    @patch('os.path.exists', side_effects=[True, False])                                                    
    def test_existing_admin_json(self):                                         
        self.assertNone(postprocess_results.create_json_file())

    # JSON file, no log file
    @patch('os.path.exists', side_effects=[True, True])                                                    
    def test_existing_admin_json(self):                                         
        ...

The third test requires an actual file system, or for you to mock open.

Answered By: chepner

So, I ended up breaking my original function into 3 different functions for easier testing.

The tests are performed by checking what the result of the ‘def create_file1’ would be when we feed it different return_values from the other 2 functions and when we add valid data.

class TestFile1JsonCreation(unittest.TestCase):
    @patch('builtins.open', new_callable=mock_open())
    @patch('os.stat')
    @patch('file1_exists', return_value=True)
    @patch('file2_exists', return_value=False)
    def test_existing_file1_json(self, file2_exists, file1_existsmock, stat, mopen):
        create_file1('.')

        # file1.json should not have been written
        mopen.assert_not_called()

    @patch('builtins.open', new_callable=mock_open())
    @patch('os.stat')
    @patch('file1_exists', return_value=False)
    @patch('file2_exists', return_value=False)
    def test_missing_file2(self, file2_exists, file1_existsmock, stat, mopen):
        create_file1('.')
        
        # file1.json should not have been written
        mopen.assert_not_called()

    @patch('builtins.open', new_callable=mock_open())
    @patch('os.stat')
    @patch('file1_exists', return_value=False)
    @patch('file2_exists', return_value=True)
    def test_write_data(self, file2_exists, file1_existsmock, stat, mopen):
        class FakeStat:
            st_mtime = 1641992788
        stat.return_value = FakeStat()

        create_file1('.')

        # file1.json should have been written
        mopen.assert_called_once_with('./file1.json', 'w')

        written_data = ''.join(
            c[1][0]
            for c in mopen().__enter__().write.mock_calls
    )
        expected_data = {"creation_timestamp": "2022-01-12T13:06:28"}
        written_dict_data = json.loads(written_data)
        self.assertEqual(written_dict_data, expected_data)
Answered By: Oluevaera
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.