使用mock测试我的函数策略

时间:2014-07-09 18:03:19

标签: unit-testing python-3.x mocking

我有一个相当复杂的(对我而言)我从另一个项目(特别是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点的评论:

  1. 他在回答中说,命令或装饰很重要。
  2. 奇怪地使用装饰器修补字典patch.dict不需要将任何对象作为参数传递给函数,就像其他装饰器一样。它必须只是在源头或其他东西修改字典。
  3. 为了我的测试,我决定使用更改上下文的with方法而不是装饰器,以便我可以使用不同的路径轻松测试不同的情况。

1 个答案:

答案 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>