如何为OSError编写单元测试?
我有一段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 个回答
还有很多其他情况会引发OSError,比如文件系统已满、权限不足、文件已经存在等等。
在这个例子中,权限问题是一个很容易利用的点——只需要把 self.logfolder
设置为一个不存在的目录,而你的程序没有写入权限,比如在*nix系统中,假设你没有根目录的写入权限:
>>> import os
>>> os.makedirs('/somedir')
OSError: [Errno 13] Permission denied: '/somedir'
另外,可以考虑一下Martin Konecny建议的重构方案。
我虽然发帖有点晚,但想分享一下我的解决方案(基于这个回答),还想附上我写的单元测试。
我写了一个函数,用来创建路径,如果路径不存在的话,类似于命令行中的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')
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)
你可以用一种更符合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%,你只需要创建一个测试用例,确保目录已经存在。