关于python:如何为OSError编写单元测试?

How do I write a unit test for OSError?

我有以下要测试的python代码:

1
2
3
4
5
6
7
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

我想在单元测试中做如下的事情。

1
2
3
4
def test_find_or_make_logfolder_pre_existing(self):
    with self.assertRaises(OSError):
        makedirs(self.logfolder)
        find_or_make_logfolder()

但是,if not path.isdir(self.logfolder):正在检查目录是否已经存在,这样,except OSError只会在某些边缘情况下抛出,即程序或人员在if之后几毫秒和try之前成功生成目录。

我该如何测试这个,还是真的需要测试这个?

当覆盖率为100%时,我倾向于喜欢它。


mock库是实现100%覆盖率的必备工具。

模拟make_dirs()功能,在其上设置side_effect功能:

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

1
2
3
4
5
6
7
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)

你可以走一条更Python的路线来达到这个目的。在Python中,哲学是

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

在这里见到EAFP

考虑到这一点,您的代码可以编写如下:

1
2
3
4
5
6
def find_or_make_logfolder(self):
    try:
        makedirs(self.logfolder)
    except OSError:
        #self.logfolder was already a directory, continue on.
        pass

现在为了100%地覆盖这些代码,您只需要在目录已经存在的地方创建一个测试用例。


我在这里发布得很晚,但我想分享我的解决方案(基于这个答案),还包括我的mocked单元测试。

我做了一个函数来创建一个路径,如果它不存在的话,比如mkdir -p(并称它为mkdir_p,以便于我记忆它)。

Myo-模

1
2
3
4
5
6
7
8
9
10
11
12
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

如果os.makedirs无法创建目录,我们检查OSError错误号。如果是errno.EEXIST(=17),并且我们看到了路径的存在,那么我们不需要做任何事情(尽管打印某些内容可能会有所帮助)。如果错误号是其他值,例如errno.EPERM(=13),那么我们会引发异常,因为目录不可用。

我通过模拟os并在测试函数中为OSError分配错误号来测试它。(根据Kenneth Reitz的建议,这使用了一个文件tests/context.py来允许从父目录轻松导入。尽管与这个问题没有直接关系,但为了完整起见,我在这里将它包括在内。)

测试/上下文.py

1
2
3
4
5
import sys
import os
sys.path.insert(0, os.path.abspath('..'))

import my_module

测试/我的模块测试.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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')

还有许多其他情况会引发OSError,例如文件系统已满、权限不足、文件已存在等。

在这种情况下,权限很容易被利用——只需将self.logfolder设置为一个不存在的目录,而您的进程对该目录没有写权限,例如在*nix中,假设您在根目录中没有写权限:

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

另外,考虑一下MartinKonecny建议的重构。