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

Nik*_*laj 62 python testing unit-testing mocking python-import

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

def main():
    pass

if __name__ == "__main__":
    main()
Run Code Online (Sandbox Code Playgroud)

我想为下半部分编写单元测试(我希望实现100%的覆盖率).我发现了runpy内置模块执行导入/ __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()
Run Code Online (Sandbox Code Playgroud)

mou*_*uad 49

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

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

有关如何排除,main()您可以编写coverage配置文件并添加到部分报告中:

[report]

exclude_lines =
    if __name__ == .__main__.:
Run Code Online (Sandbox Code Playgroud)

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

希望这可以提供帮助.

  • 恕我直言,即使我发现它有趣和有用,这个答案确实*不*实际上对OP做出回应.他想测试main被调用,而不是跳过这个检查.否则,脚本实际上可以做任何事情,除了实际预期,当启动时,测试说"好,一切正常!".主要功能可以完全进行单元测试,即使从未实际调用过. (8认同)
  • 它可能不会对 OP 做出回应,但对于实际目的来说,这是一个很好的答案,至少我是这样发现这个问题的。类似的解决方案是使用 `# pragma: no cover`,就像这样 `if __name__ == '__main__': # pragma: no cover`。就我个人而言,我不愿意这样做,因为它使代码混乱并且非常丑陋,所以我认为 mouad 的答案是最好的解决方案,但其他人可能会发现它很有用。 (2认同)

Dav*_*nan 12

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

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

import imp
runpy = imp.load_source('__main__', '/path/to/runpy.py')
Run Code Online (Sandbox Code Playgroud)

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

  • imp模块似乎与我在问题中使用的runpy模块非常相似.问题是在加载模块之后和代码运行之前,模拟不能(显然)插入.你对此有什么建议吗? (5认同)

rob*_*bru 6

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

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

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

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

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())
Run Code Online (Sandbox Code Playgroud)

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

run_script(__name__, __doc__, main)
Run Code Online (Sandbox Code Playgroud)

从技术上讲,无论您的脚本是作为模块导入还是作为脚本运行,此函数都将无条件运行.这是可以的,因为除非脚本作为脚本运行,否则该函数实际上不会执行任何操作.所以代码覆盖看到函数运行并说"是的,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)
Run Code Online (Sandbox Code Playgroud)

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

  • 我很欣赏找到解决方案的创造力和坚持不懈,但如果你在我的团队中,我会否决这种编码方式.Python的优势之一是它具有高度惯用性.`if __name__ == ...`是**让模块脚本的方式.任何pythonista都会识别该行,并了解它的作用.你的解决方案只是在没有任何理由的情况下混淆显而易见的事情,除了抓挠知识分子痒.正如我所说:一个聪明的解决方案,但_clever_并不总是等同于_correct_. (15认同)
  • 如果块中的任何**逻辑**在`if __name__ ...`下缩进,那么你做错了并且应该重构.`if __name __...`下的唯一代码行应该是:`main()`. (8认同)
  • @mac 我不知道我是否同意这一点。是的,如果你有**逻辑**,你应该重构。但这并不意味着“if __name__ ...”下唯一可以拥有的就是“main()”。例如,我喜欢使用 argeparse 并在 `if __name__ ...` 部分构建我的解析器。然后抽象我的 main 以使用显式参数而不是类似:“main(parser.parse_args())”。如果需要的话,这使得从另一个模块调用“main()”变得更容易。否则,您必须构造一个 `argeparse.Namespace()` 对象并让所有默认参数正确。或者有更惯用的方法吗? (2认同)

Sam*_*rks 5

Python 3 解决方案:

import os
from importlib.machinery import SourceFileLoader
from importlib.util import spec_from_loader, module_from_spec
from importlib import reload
from unittest import TestCase
from unittest.mock import MagicMock, patch
    

class TestIfNameEqMain(TestCase):
    def test_name_eq_main(self):
        loader = SourceFileLoader('__main__',
                                  os.path.join(os.path.dirname(os.path.dirname(__file__)),
                                               '__main__.py'))
        with self.assertRaises(SystemExit) as e:
            loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
Run Code Online (Sandbox Code Playgroud)

使用定义自己的小函数的替代解决方案:

# module.py
def main():
    if __name__ == '__main__':
        return 'sweet'
    return 'child of mine'
Run Code Online (Sandbox Code Playgroud)

您可以使用以下方法进行测试:

# Override the `__name__` value in your module to '__main__'
with patch('module_name.__name__', '__main__'):
    import module_name
    self.assertEqual(module_name.main(), 'sweet')

with patch('module_name.__name__', 'anything else'):
    reload(module_name)
    del module_name
    import module_name
    self.assertEqual(module_name.main(), 'child of mine')
Run Code Online (Sandbox Code Playgroud)