如何为OSError编写单元测试?

1 投票
4 回答
3111 浏览
提问于 2025-04-18 08:01

我有一段Python代码想要测试:

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

我想在我的单元测试中做类似下面的事情。

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%。

4 个回答

0

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

在这个例子中,权限问题是一个很容易利用的点——只需要把 self.logfolder 设置为一个不存在的目录,而你的程序没有写入权限,比如在*nix系统中,假设你没有根目录的写入权限:

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

另外,可以考虑一下Martin Konecny建议的重构方案。

1

我虽然发帖有点晚,但想分享一下我的解决方案(基于这个回答),还想附上我写的单元测试。

我写了一个函数,用来创建路径,如果路径不存在的话,类似于命令行中的mkdir -p(我把它叫做mkdir_p,这样更容易记住)。

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

如果os.makedirs无法创建目录,我们会检查OSError的错误编号。如果这个编号是errno.EEXIST(等于17),并且我们看到路径已经存在,那就没必要再做什么(不过打印一些信息可能会有帮助)。如果错误编号是其他的,比如errno.EPERM(等于13),那么我们就会抛出异常,因为这个目录是不可用的。

我通过模拟os模块,并在测试函数中给OSError分配错误编号来进行测试。(这里用到了一个文件tests/context.py,方便从父目录导入,正如Kenneth Reitz所建议的。虽然这和问题没有直接关系,但我还是把它放在这里,以便完整。)

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')
4

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

可以把 make_dirs() 这个函数做个“假装”,并为它设置一个side_effect

side_effect 让你在调用这个假装的函数时,可以执行一些额外的操作,比如抛出一个异常。

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)
2

你可以用一种更符合Python风格的方法来实现这个。Python的理念是:

与其请求许可,不如请求原谅。

想了解更多,可以查看这里的EAFP

考虑到这一点,你的代码可以这样写:

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

要让这段代码的覆盖率达到100%,你只需要创建一个测试用例,确保目录已经存在。

撰写回答