0

I readily admit to going a bit overboard with unit testing. While I have passing tests, I find my solution to be inelegant, and I'm curious if anyone has a cleaner solution.

The class being tested:

class Config():

    def __init__(self):
        config_parser = ConfigParser()
        try:
            self._read_config_file(config_parser)
        except FileNotFoundError as e:
            pass

        self.token = config_parser.get('Tokens', 'Token', )

    @staticmethod
    def _read_config_file(config):
        if not config.read(os.path.abspath(os.path.join(BASE_DIR, ROOT_DIR, CONFIG_FILE))):
            raise FileNotFoundError(f'File {CONFIG_FILE} not found at path {BASE_DIR}{ROOT_DIR}')

The ugly test:

class TestConfiguration(unittest.TestCase):

    @mock.patch('config.os.path.abspath')
    def test_config_init_sets_token(self, mockFilePath: mock.MagicMock):
        with open('mock_file.ini', 'w') as file: #here's where it gets ugly
            file.write('[Tokens]\nToken: token')
        mockFilePath.return_value = 'mock_file.ini'

        config = Config()

        self.assertEqual(config.token, 'token')
        os.remove('mock_file.ini') #quite ugly

EDIT: What I mean is I'm creating a file instead of mocking one. Does anyone know how to mock a file object, while having its data set so that it reads ascii text? The class is deeply buried. Other than that, the way ConfigParser sets data with .read() is throwing me off. Granted, the test "works", it doesn't do it nicely.

For those asking about other testing behaviors, here's an example of another test in this class:

@mock.patch('config.os.path.abspath')
def test_warning_when_file_not_found(self, mockFilePath: mock.MagicMock):
    mockFilePath.return_value = 'mock_no_file.ini'

    with self.assertRaises(FileNotFoundError):
        config.Config._read_config_file(ConfigParser())

Thank you for your time.

Jordon Birk
  • 480
  • 1
  • 9
  • 28
  • That doesn't seem unreasonable. For a different solution, couldn't you just make `BASE_DIR`, `ROOT_DIR`, and `CONFIG_FILE` something that you can set in the module before instantiating a `Config()` instance? What if your `Config` class accepted as an optional parameter a path to a configuration file? – larsks Sep 07 '17 at 20:45
  • 1
    I don't think you should be testing `ConfigParser`. You should rather be testing that `_read_config_file` does what you mean it to do which is, read a configuration file and raise an exception when it fails to do that. – Oluwafemi Sule Sep 07 '17 at 20:49
  • Thanks, @larsks. I see the utility in that, too, but, for now, I'm interested in keeping all of my config-relevant info in one place (i.e., keep the name of the `.ini` file in the `.ini` file). What's really giving me trouble, here, is how the `ConfigParser` class sets file information when it calls `.read`. I'd rather just patch a mocked file object that takes ascii chars to set data, but the way the class is designed doesn't make that intuitive. – Jordon Birk Sep 07 '17 at 20:58
  • @OluwafemiSule - I did test that, as well, with several variants of the above as follows: `with self.assertRaises(FileNotFoundError):` `config.Config._read_config_file(config.ConfigParser())` *(weird syntax to get around the hidden method prefix) – Jordon Birk Sep 07 '17 at 21:02
  • *typo above, (i.e., keep the name of the .ini file in the **Config** file) – Jordon Birk Sep 07 '17 at 21:08

1 Answers1

2

I've found it!

I had to start off with a few imports:from io import TextIOWrapper, BytesIO

This allows a file object to be created: TextIOWrapper(BytesIO(b'<StringContentHere>'))

The next part involved digging into the configparser module to see that it calls open(), in order to mock.patch the behavior, and, here we have it, an isolated unittest!

@mock.patch('configparser.open') 
def test_bot_init_sets_token(self, mockFileOpen: mock.MagicMock):

    mockFileOpen.return_value = TextIOWrapper(BytesIO(b'[Tokens]\nToken: token'))

    config = Config()

    self.assertEqual(config.token, 'token')
Jordon Birk
  • 480
  • 1
  • 9
  • 28