1

I have the following python code which I want to test:

def find_or_make_logfolder(self):
    if not path.isdir(self.logfolder):
        try:
            makedirs(self.logfolder)
        except OSError:
            if not path.isdir(self.logfolder):
                raise

I want to do something like the following in my unittest.

def test_find_or_make_logfolder_pre_existing(self):
    with self.assertRaises(OSError):
        makedirs(self.logfolder)
        find_or_make_logfolder()

However, if not path.isdir(self.logfolder): is checking if the directory already exists or not so that the except OSError will only be thrown in some edge case where a program or person succeeds in making the directory a few milliseconds after the if and before the try.

How do I test this, or do I really need to test this?

I tend to like it when coverage says 100%.

alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
nu everest
  • 9,589
  • 12
  • 71
  • 90

4 Answers4

4

mock library is a must-have tool for achieving a 100% coverage.

Mock out make_dirs() function and set a side_effect on it:

side_effect allows you to perform side effects, including raising an exception when a mock is called

from mock import patch  # or from unittest import mock for python-3.x

@patch('my_module.makedirs')
def test_find_or_make_logfolder_pre_existing(self, makedirs_mock):
    makedirs_mock.side_effect = OSError('Some error was thrown')
    with self.assertRaises(OSError):
        makedirs(self.logfolder)
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
2

You could go a more Pythonic route to achieve this. In Python the philosophy is

It's better to ask for forgiveness than to ask for permission.

See EAFP here

With that in mind, your code could be written as follows:

def find_or_make_logfolder(self):
    try:
        makedirs(self.logfolder)
    except OSError:
        #self.logfolder was already a directory, continue on.
        pass

Now to get 100% of this code covered, you will just need to create a test case where the directory already exists.

Martin Konecny
  • 57,827
  • 19
  • 139
  • 159
1

I'm late posting here, but I wanted to share my solution (based on this answer), and also include my mocked unit tests.

I made a function to create a path if it doesn't exist, like mkdir -p (and called it mkdir_p to facilitate my remembering it).

my_module.py

import os
import errno

def mkdir_p(path):
    try:
        print("Creating directory at '{}'".format(path))
        os.makedirs(path)
    except OSError as e:
        if e.errno == errno.EEXIST and os.path.isdir(path):
            print("Directory already exists at '{}'".format(path))
        else:
            raise

If os.makedirs is unable to create the directory, we check the OSError error number. If it is errno.EEXIST (==17), and we see that the path exists, we don't need to do anything (although printing something could be helpful). If the error number is something else, e.g. errno.EPERM (==13), then we raise the exception, because the directory is not available.

I'm testing it by mocking os and assigning error numbers to OSError in the test functions. (This uses a file tests/context.py to allow easy importing from the parent directory, as suggested by Kenneth Reitz. Although not directly related to the question, I'm including it here for the sake of completeness.)

tests/context.py

import sys
import os
sys.path.insert(0, os.path.abspath('..'))

import my_module

tests/my_module_tests.py

import errno
import unittest

import mock

from .context import my_module

@mock.patch('my_module.os')
class MkdirPTests(unittest.TestCase):
    def test_with_valid_non_existing_dir(self, mock_os):
        my_module.mkdir_p('not_a_dir')
        mock_os.makedirs.assert_called_once_with('not_a_dir')

    def test_with_existing_dir(self, mock_os):
        mock_os.makedirs.side_effect = OSError(errno.EEXIST, 'Directory exists.')
        mock_os.path.isdir.return_value = True
        my_module.mkdir_p('existing_dir')
        mock_os.path.isdir.assert_called_once_with('existing_dir')

    def test_with_permissions_error(self, mock_os):
        mock_os.makedirs.side_effect = OSError(errno.EPERM, 'You shall not pass!')
        with self.assertRaises(OSError):
            my_module.mkdir_p('forbidden_dir')
Community
  • 1
  • 1
jared
  • 141
  • 10
  • I don't think this example "assigns error numbers", the error numbers are passed as args, which doesn't do anything special. Python 3.6.9: `In [1]: err = OSError(os.errno.EEXIST)`, `In [2]: err.errno is None` `Out[2]: True`, `In [3]: err.args` `Out[3]: (17,)` – JHS Jun 24 '20 at 20:25
0

There are plenty of other situations that raise OSError, e.g. filesystem full, insufficient permissions, file already exists etc.

In this case permissions is an easy one to exploit - simply arrange for self.logfolder to be set to a nonexistent directory for which your process does not have write permission, e.g. in *nix assuming that you do not have write permission in the root directory:

>>> import os
>>> os.makedirs('/somedir')
OSError: [Errno 13] Permission denied: '/somedir'

Also, consider Martin Konecny's suggested refactor.

mhawke
  • 84,695
  • 9
  • 117
  • 138
  • p.s. This just bit me on a unittest. Someone ran the test suite and the exception was not raised. Their box has major issues and this may assume non-insane permissions- but it is still an assumption and one proven to be false. I now am explicitly setting up a temp dir with guaranteed no permissions. – mateor Feb 19 '15 at 18:17
  • The assumption was mentioned in the answer. Using /root was just a suggestion because it is a known directory that non-root users _shouldn't_ be able to write to. Might be better to look at mock as suggested by alecxe – mhawke Feb 19 '15 at 22:44
  • Yeah, I basically rolled something similar, made a temp dir and set the permissions manually. I wasn't using /root in my test, I used a fake dir (/should/not/exist or some such). – mateor Feb 20 '15 at 02:31