Unit testing 使用mock测试my函数的策略
我从另一个项目(特别是从其他项目)复制了一组相当复杂的函数(无论如何,对我来说)。这些函数在系统上检查给定二进制文件的存在和状态。它们按原样工作,但我真的想在我的项目中为它们编写适当的测试。为此,我使用了Python3.4和unittest.mock。因此,在checks.py模块中,我有以下功能: 更新:更改了最终测试代码中函数命名中的一些样式项,请参见下文Unit testing 使用mock测试my函数的策略,unit-testing,python-3.x,mocking,Unit Testing,Python 3.x,Mocking,我从另一个项目(特别是从其他项目)复制了一组相当复杂的函数(无论如何,对我来说)。这些函数在系统上检查给定二进制文件的存在和状态。它们按原样工作,但我真的想在我的项目中为它们编写适当的测试。为此,我使用了Python3.4和unittest.mock。因此,在checks.py模块中,我有以下功能: 更新:更改了最终测试代码中函数命名中的一些样式项,请参见下文 import os def is_executable(fpath): ''' Returns true if th
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
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
这些程序执行得很好,它们将检查二进制文件的路径,看它是否可执行,并返回第一个结果。基本上是重新创建Linux“which”命令
到目前为止,我的测试模块如下所示:
注意:请原谅不同的函数名样式,在最终结果中更新,请参见下文
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
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 points的评论:
patch.dict
与其他decorator一样,不需要将任何对象作为参数传递到函数中。它必须在源代码或其他地方修改dict和方法来更改上下文,而不是decorator,这样我就可以使用不同的路径轻松测试不同的案例
@mock.patch('function.one')
@mock.patch('function.two')
def test_something(self, mock_function_two, mock_function_one):
<test code>
免责声明:我还没有实际测试过这段代码,所以请将它更多地作为一个指南,而不是一个保证有效的解决方案,但希望它能帮助您朝着正确的方向前进。这真的很有帮助,我没有意识到装饰者的顺序很重要。我现在更接近了。我确实有一个新问题,但是我以前遇到过。作为装饰程序尝试修补
mock.patch.dict
时,我的函数总是抱怨没有足够的位置参数:TypeError:test\u queryPATH()缺少1个必需的位置参数:“mock\u environ”
。当我在教程中看到这个例子时,每个人都会对dicts使用“with”上下文管理器。知道修饰符为什么不向我的函数发送arg吗?我认为您可能需要从test\u queryPATH
的签名中删除mock\u environ
参数,看起来框架在模拟字典时不会将模拟对象传递到测试中。根据记录,您的说法似乎是正确的。使用装饰器修补patch.dict
,不需要将任何对象传入函数。我最终将实际使用“with”上下文管理器,这样我就可以在同一个测试中使用不同的路径测试不同的案例,但是您的帮助非常到位。我将更新我的最终代码,以供将来在这个问题上参考。嗨,我刚刚遇到过一个类似的情况,我不得不模拟许多与操作系统相关的调用@mock.patch('os.listdir')
@mock.patch('os.access')@mock.patch('os.path.exists')
@mock.patch('os.path.isfile')
mock.isfile.return\u value=Truemock\u access.return\u value=True
mock\u exists.return\u value=True
我们不能用这些来代替它们吗('os')patch\u os.listdir.return\u value=True
patch\u os.access.return\u value=True
patch\u os.path.exists.return\u value=True
好处是,测试的参数更少。这种方法有什么问题吗?