我有一个相当复杂的(对我而言)我从另一个项目(特别是subuser)复制的一组函数。这些函数对系统进行一些检查,以确定给定二进制文件的存在和状态。它们按原样工作,但我真的想在我的项目中为它们编写适当的测试。我正在使用python 3.4和unittest.mock。所以在我的checks.py模块中,我有这些功能:
更新:在最终测试代码中更改了函数命名中的一些样式项,请参阅下文。
import os
def is_executable(fpath):
'''
Returns true if the given filepath points to an executable file.
'''
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
# Origonally taken from: http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
def query_path(test):
'''
Search the PATH for an executable.
Given a function which takes an absolute filepath and returns True when the
filepath matches the query, return a list of full paths to matched files.
'''
matches = []
def append_if_matches(exeFile):
if is_executable(exeFile):
if test(exeFile):
matches.append(exeFile)
for path in os.environ['PATH'].split(os.pathsep):
path = path.strip('"')
if os.path.exists(path):
for fileInPath in os.listdir(path):
exeFile = os.path.join(path, fileInPath)
append_if_matches(exeFile)
return matches
def which(program):
'''
Check for existence and executable state of program.
'''
fpath, fname = os.path.split(program)
if not fpath == '':
if is_executable(program):
return program
else:
def matches_program(path):
fpath, fname = os.path.split(path)
return program == fname
programMatches = query_path(matches_program)
if len(programMatches) > 0:
return programMatches[0]
return None
这些表现良好,他们会检查PATH的二进制文件,看看它是否可执行,并返回第一个结果。基本上重新创建Linux'which'命令。
到目前为止,我的测试模块如下所示:
注意:原谅不同的函数名称样式,在最终结果中更新,见下文。
import unittest
import unittest.mock as mock
from myproject import checks
class TestSystemChecks(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
# This test works great
@mock.patch('os.path.isfile')
@mock.patch('os.access')
def test_isExecutable(self, mock_isfile, mock_access):
# case 1
mock_isfile.return_value = True
mock_access.return_value = True
self.assertTrue(
checks.isExecutable('/some/executable/file'))
# case 2
mock_isfile.return_value = True
mock_access.return_value = False
self.assertFalse(
checks.isExecutable('/some/non/executable/file'))
# THIS ONE IS A MESS.
@mock.patch('os.path.isfile')
@mock.patch('os.path.exists')
@mock.patch('os.access')
@mock.patch('os.listdir')
@mock.patch('os.environ')
def test_queryPATH(
self, mock_isfile, mock_access, mock_environ, mock_exists,
mock_listdir):
# case 1
mock_isfile.return_value = True
mock_access.return_value = True
mock_exists.return_value = True
mock_listdir.return_value = [
'somebin',
'another_bin',
'docker']
mock_environ.dict['PATH'] = \
'/wrong:' +\
'/wrong/path/two:' +\
'/docker/path/one:' +\
'/other/docker/path'
target_paths = [
'/docker/path/one/docker',
'/other/docker/path/docker']
def isPathToDockerCommand(path):
return True
self.assertEqual(
target_paths,
checks.queryPATH(isPathToDockerCommand))
def test_which(self):
pass
所以queryPATH()的测试是我的问题。我是否试图在一个功能中做太多事情?我是否真的需要每次都重新创建所有这些模拟对象,或者有没有办法在setUp()中为所有这些测试设置元对象(或一组对象)?或者,也许我仍然不明白原始代码是如何工作的,我只是没有正确设置我的测试(但使用模拟对象是正确的)。运行此测试的结果产生:
checks.queryPATH(isPathToDockerCommand))
AssertionError: Lists differ: ['/docker/path/one/docker', '/other/docker/path/docker'] != []
First list contains 2 additional elements.
First extra element 0:
/docker/path/one/docker
- ['/docker/path/one/docker', '/other/docker/path/docker']
+ []
由于测试的复杂性和功能本身,我不确定为什么我无法正确设计我的测试。这是我第一次在单元测试中广泛使用mock,并希望在继续我的项目之前完成它,这样我就可以从一开始就编写TDD样式。谢谢!
更新:已解决
这是我的最终结果看起来就像这三个功能的所有荣耀一样。
import unittest
import unittest.mock as mock
from myproject import checks
class TestSystemChecks(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
@mock.patch('os.access')
@mock.patch('os.path.isfile')
def test_is_executable(self,
mock_isfile,
mock_access):
# case 1
mock_isfile.return_value = True
mock_access.return_value = True
self.assertTrue(
checks.is_executable('/some/executable/file'))
# case 2
mock_isfile.return_value = True
mock_access.return_value = False
self.assertFalse(
checks.is_executable('/some/non/executable/file'))
@mock.patch('os.listdir')
@mock.patch('os.access')
@mock.patch('os.path.exists')
@mock.patch('os.path.isfile')
def test_query_path(self,
mock_isfile,
mock_exists,
mock_access,
mock_listdir):
# case 1
# assume file exists, and is in all paths supplied
mock_isfile.return_value = True
mock_access.return_value = True
mock_exists.return_value = True
mock_listdir.return_value = ['docker']
fake_path = '/path/one:' +\
'/path/two'
def is_path_to_docker_command(path):
return True
with mock.patch.dict('os.environ', {'PATH': fake_path}):
self.assertEqual(
['/path/one/docker', '/path/two/docker'],
checks.query_path(is_path_to_docker_command))
# case 2
# assume file exists, but not in any paths
mock_isfile.return_value = True
mock_access.return_value = True
mock_exists.return_value = False
mock_listdir.return_value = ['docker']
fake_path = '/path/one:' +\
'/path/two'
def is_path_to_docker_command(path):
return True
with mock.patch.dict('os.environ', {'PATH': fake_path}):
self.assertEqual(
[],
checks.query_path(is_path_to_docker_command))
# case 3
# assume file does not exist
mock_isfile.return_value = False
mock_access.return_value = False
mock_exists.return_value = False
mock_listdir.return_value = ['']
fake_path = '/path/one:' +\
'/path/two'
def is_path_to_docker_command(path):
return True
with mock.patch.dict('os.environ', {'PATH': fake_path}):
self.assertEqual(
[],
checks.query_path(is_path_to_docker_command))
@mock.patch('os.listdir')
@mock.patch('os.access')
@mock.patch('os.path.exists')
@mock.patch('os.path.isfile')
def test_which(self,
mock_isfile,
mock_exists,
mock_access,
mock_listdir):
# case 1
# file exists, only take first result
mock_isfile.return_value = True
mock_access.return_value = True
mock_exists.return_value = True
mock_listdir.return_value = ['docker']
fake_path = '/path/one:' +\
'/path/two'
with mock.patch.dict('os.environ', {'PATH': fake_path}):
self.assertEqual(
'/path/one/docker',
checks.which('docker'))
# case 2
# file does not exist
mock_isfile.return_value = True
mock_access.return_value = True
mock_exists.return_value = False
mock_listdir.return_value = ['']
fake_path = '/path/one:' +\
'/path/two'
with mock.patch.dict('os.environ', {'PATH': fake_path}):
self.assertEqual(
None,
checks.which('docker'))
对@robjohncox点的评论:
patch.dict
不需要将任何对象作为参数传递给函数,就像其他装饰器一样。它必须只是在源头或其他东西修改字典。with
方法而不是装饰器,以便我可以使用不同的路径轻松测试不同的情况。答案 0 :(得分:1)
关于"我试图在一个功能中做太多事情",我认为答案是否定的,你在这里测试一个单元,看起来不可能进一步打破复杂性 - 测试的复杂性仅仅是您的功能所需的复杂环境设置的结果。事实上,我赞赏你的努力,大多数人都会考虑这个功能,并且认为太难了,让我们不要为这些测试付出代价"。
关于可能导致测试失败的原因,有两件事情会突然出现:
在测试方法签名中对模拟参数进行排序:您必须注意签名中每个模拟的位置,模拟对象将由反向中的模拟框架传入在装饰器中声明它们的顺序,例如:
@mock.patch('function.one')
@mock.patch('function.two')
def test_something(self, mock_function_two, mock_function_one):
<test code>
我在每个函数中都看到你没有按正确的顺序使用模拟参数(虽然它适用于你的第一个例子,test_isExecutable
,因为两个模拟返回值都是True
在环境词典中模拟PATH :我不认为在那里采取的方法可以用来模仿os.environ
,因为它的设置方式我不会# 39; t认为在测试代码中调用os.environ['PATH']
时会返回您的期望(尽管我可能错了)。幸运的是,模拟应该让你在这里用@mock.patch.dict
装饰器覆盖,如下所示。这与将模拟参数按正确的顺序放在一起应该会产生类似的结果:
fake_path = '/wrong:' +\
'/wrong/path/two:' +\
'/docker/path/one:' +\
'/other/docker/path'
@mock.patch.dict('os.environ', {'PATH': fake_path})
@mock.patch('os.listdir')
@mock.patch('os.access')
@mock.patch('os.path.exists')
@mock.patch('os.path.isfile')
def test_queryPATH(self,
mock_isfile,
mock_exists,
mock_access,
mock_listdir,
mock_environ):
mock_isfile.return_value = True
mock_access.return_value = True
mock_exists.return_value = True
mock_listdir.return_value = [
'somebin',
'another_bin',
'docker']
target_paths = [
'/docker/path/one/docker',
'/other/docker/path/docker']
def isPathToDockerCommand(path):
return True
self.assertEqual(
target_paths,
checks.queryPATH(isPathToDockerCommand))
免责声明:我还没有真正测试过这段代码,所以请把它作为指导而不是保证工作的解决方案,但希望它可以帮助你朝着正确的方向前进。 / p>