Search code examples
pythonunit-testingmocking

Mocking builtins.open within a tested class causes issues with pytz


I am finding myself unable to mock opening a file in a class that then makes a call to pytz to set a timezone, as pytz also needs to do a file open operation, and ends up also receiving a mock. An example of the issue:

import unittest
from unittest.mock import patch, mock_open
from datetime import datetime
import pytz


class Foo:
    def __init__(self, filename):
        with open(filename, "r") as input:
            timestring = input.read()
            time = datetime.strptime(timestring, '%Y-%m-%d %H:%M:%S')
            zone = pytz.timezone('GMT')
            self.converted_time = zone.localize(time).strftime('%a %b %d %H:%M:%S %Z %Y')

    def show_time(self):
        return self.converted_time


class TestFoo(unittest.TestCase):
    def test_foo(self):
        with patch('builtins.open', mock_open(read_data="2023-12-20 00:00:00")) as mock_file:
            foo = Foo(mock_file)
            output = foo.converted_time
            self.assertEqual(output, 'Wed Dec 20 00:00:00 GMT 2023')


if __name__ == '__main__':
    unittest.main()

The test fails when it reaches zone = pytz.timezone('GMT') as pytz also tries to read from the mock that opened the file originally. Is there a way to restrict the mocking only to the Foo class, and not any function that is called from another module? Or alternatively, to mock only the first usage of builtins.open?


Solution

  • It would be best to redesign your class to not require a mock in order to test it. Foo.__init__ should take a file-like object as an argument instead of trying to open a file itself.

    import io
    
    
    class Foo:
        def __init__(self, fobj):
            timestring = fobj.read()
            time = datetime.strptime(timestring, '%Y-%m-%d %H:%M:%S')
            zone = pytz.timezone('GMT')
            return zone.localize(time).strftime('%a %b %d %H:%M:%S %Z %Y')
    
    
    class TestFoo(unittest.TestCase):
        def test_foo(self):
            output = Foo(io.StringIO("2023-12-20 00:00:00"))
            self.assertEqual(output, 'Wed Dec 20 00:00:00 GMT 2023')