Unit testing 使用mock测试my函数的策略

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

我从另一个项目(特别是从其他项目)复制了一组相当复杂的函数(无论如何,对我来说)。这些函数在系统上检查给定二进制文件的存在和状态。它们按原样工作,但我真的想在我的项目中为它们编写适当的测试。为此,我使用了Python3.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

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的评论:

  • 他在回答中说,订购或装饰很重要
  • 奇怪的是,使用decorator修补dictionary
    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=True
    mock\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
    好处是,测试的参数更少。这种方法有什么问题吗?