如何测试或模拟“if __name__ =='__ main__'”内容

问题描述 投票:59回答:5

假设我有一个包含以下内容的模块:

def main():
    pass

if __name__ == "__main__":
    main()

我想为下半部分编写单元测试(我希望实现100%的覆盖率)。我发现runpy内置模块执行import / __name__设置机制,但我无法弄清楚如何模拟或以其他方式检查main()函数是否被调用。

这是我到目前为止所尝试的:

import runpy
import mock

@mock.patch('foobar.main')
def test_main(self, main):
    runpy.run_module('foobar', run_name='__main__')
    main.assert_called_once_with()
python unit-testing testing mocking python-import
5个回答
46
投票

我将选择另一种替代方法,即从覆盖率报告中排除if __name__ == '__main__',当然,只有在测试中已经有main()函数的测试用例时才能这样做。

至于为什么我选择排除而不是为整个脚本编写一个新的测试用例是因为如果我说你已经有一个main()函数的测试用例,你为脚本添加了另一个测试用例(仅用于100%覆盖率)只是一个重复的。

有关如何排除if __name__ == '__main__',您可以编写一个coverage配置文件并添加到部分报告中:

[report]

exclude_lines =
    if __name__ == .__main__.:

有关coverage配置文件的更多信息,请访问here

希望这可以提供帮助。


12
投票

您可以使用imp模块而不是import语句来执行此操作。 import语句的问题在于,在您有机会分配给'__main__'之前,runpy.__name__的测试作为import语句的一部分运行。

例如,您可以像这样使用imp.load_source()

import imp
runpy = imp.load_source('__main__', '/path/to/runpy.py')

第一个参数分配给导入模块的__name__


6
投票

哇,我参加派对有点晚了,但我最近遇到了这个问题,我想我想出了一个更好的解决方案,所以这里......

我正在开发一个包含十几个脚本的模块,所有脚本都以这个精确的copypasta结尾:

if __name__ == '__main__':
    if '--help' in sys.argv or '-h' in sys.argv:
        print(__doc__)
    else:
        sys.exit(main())

不可怕,当然,但也不可测试。我的解决方案是在我的一个模块中编写一个新函数:

def run_script(name, doc, main):
    """Act like a script if we were invoked like a script."""
    if name == '__main__':
        if '--help' in sys.argv or '-h' in sys.argv:
            sys.stdout.write(doc)
        else:
            sys.exit(main())

然后将此gem放在每个脚本文件的末尾:

run_script(__name__, __doc__, main)

从技术上讲,无论您的脚本是作为模块导入还是作为脚本运行,此函数都将无条件运行。这是可以的,因为除非脚本作为脚本运行,否则该函数实际上不会执行任何操作。所以代码覆盖看到函数运行并说“是的,100%的代码覆盖率!”同时,我写了三个测试来覆盖函数本身:

@patch('mymodule.utils.sys')
def test_run_script_as_import(self, sysMock):
    """The run_script() func is a NOP when name != __main__."""
    mainMock = Mock()
    sysMock.argv = []
    run_script('some_module', 'docdocdoc', mainMock)
    self.assertEqual(mainMock.mock_calls, [])
    self.assertEqual(sysMock.exit.mock_calls, [])
    self.assertEqual(sysMock.stdout.write.mock_calls, [])

@patch('mymodule.utils.sys')
def test_run_script_as_script(self, sysMock):
    """Invoke main() when run as a script."""
    mainMock = Mock()
    sysMock.argv = []
    run_script('__main__', 'docdocdoc', mainMock)
    mainMock.assert_called_once_with()
    sysMock.exit.assert_called_once_with(mainMock())
    self.assertEqual(sysMock.stdout.write.mock_calls, [])

@patch('mymodule.utils.sys')
def test_run_script_with_help(self, sysMock):
    """Print help when the user asks for help."""
    mainMock = Mock()
    for h in ('-h', '--help'):
        sysMock.argv = [h]
        run_script('__main__', h*5, mainMock)
        self.assertEqual(mainMock.mock_calls, [])
        self.assertEqual(sysMock.exit.mock_calls, [])
        sysMock.stdout.write.assert_called_with(h*5)

布拉姆!现在,您可以编写一个可测试的main(),将其作为脚本调用,具有100%的测试覆盖率,而不需要忽略覆盖率报告中的任何代码。


2
投票

一种方法是将模块作为脚本运行(例如os.system(...)),并将它们的stdout和stderr输出与预期值进行比较。


0
投票

我的解决方案是使用imp.load_source()并强制在main()早期提出异常,因为它没有提供必需的CLI参数,提供格式错误的参数,设置路径以找不到所需的文件等。

import imp    
import os
import sys

def mainCond(testObj, srcFilePath, expectedExcType=SystemExit, cliArgsStr=''):
    sys.argv = [os.path.basename(srcFilePath)] + (
        [] if len(cliArgsStr) == 0 else cliArgsStr.split(' '))
    testObj.assertRaises(expectedExcType, imp.load_source, '__main__', srcFilePath)

然后在您的测试类中,您可以像这样使用此函数:

def testMain(self):
    mainCond(self, 'path/to/main.py', cliArgsStr='-d FailingArg')
© www.soinside.com 2019 - 2024. All rights reserved.