繁体   English   中英

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

[英]How to test or mock "if __name__ == '__main__'" contents

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

def main():
    pass

if __name__ == "__main__":
    main()

我想为下半部分编写单元测试(我想实现 100% 的覆盖率)。 我发现了执行 import/ __name__设置机制的runpy内置模块,但我不知道如何模拟或以其他方式检查是否调用了main() function。

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

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()

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

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

对于如何排除if __name__ == '__main__'您可以编写一个覆盖配置文件并在部分报告中添加:

[report]

exclude_lines =
    if __name__ == .__main__.:

有关覆盖配置文件的更多信息可以在这里找到。

希望这能有所帮助。

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

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

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

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

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

我正在研究一个包含十几个脚本的模块,所有这些脚本都以这个完全相同的 copypasta 结尾:

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

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

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)

从技术上讲,无论您的脚本是作为模块导入还是作为脚本运行,此 function 都将无条件运行。 但这没关系,因为 function 实际上不做任何事情,除非脚本作为脚本运行。 所以代码覆盖率看到 function 运行并说“是的,100% 代码覆盖率”同时:我编写了三个测试来覆盖 function 本身:

@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% 的测试覆盖率,并且无需忽略覆盖率报告中的任何代码。

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)))

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

# module.py
def main():
    if __name__ == '__main__':
        return 'sweet'
    return 'child of mine'

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

# 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')

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

我不想排除有问题的行,因此基于对解决方案的解释,我实现了此处给出的替代答案的简化版本...

  1. 我将if __name__ == "__main__":包装在 function 中以使其易于测试,然后调用 function 以保留逻辑:
# myapp.module.py

def main():
    pass

def init():
    if __name__ == "__main__":
        main()

init()
  1. 我使用unittest.mock模拟了__name__以获得有问题的行:
from unittest.mock import patch, MagicMock
from myapp import module

def test_name_equals_main():
  # Arrange
  with patch.object(module, "main", MagicMock()) as mock_main:
    with patch.object(module, "__name__", "__main__"):
         # Act
         module.init()

  # Assert
  mock_main.assert_called_once()

如果您将 arguments 发送到模拟的 function 中,就像这样,

if __name__ == "__main__":
    main(main_args)

那么您可以使用assert_called_once_with()进行更好的测试:

expected_args = ["expected_arg_1", "expected_arg_2"]
mock_main.assert_called_once_with(expected_args)

如果需要,您还可以像这样向MagicMock()添加return_value

with patch.object(module, "main", MagicMock(return_value='foo')) as mock_main:

我的解决方案是使用imp.load_source()并通过不提供必需的 CLI 参数、提供格式错误的参数、以找不到所需文件的方式设置路径等方式强制在main()早期引发异常.

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)

然后在您的测试 class 中,您可以像这样使用这个 function:

def testMain(self):
    mainCond(self, 'path/to/main.py', cliArgsStr='-d FailingArg')

我发现这个解决方案很有帮助。 如果您使用 function 保留所有脚本代码,则效果很好。 该代码将作为一个代码行处理。 是否为覆盖率计数器执行了整行并不重要(尽管这并不是你实际期望的 100% 覆盖率)这个技巧也被接受 pylint。 ;-)

if __name__ == '__main__': \
    main()

如果只是为了获得 100% 并且那里没有什么“真实的”可以测试,那么忽略那条线会更容易。

如果您使用的是常规覆盖库,您只需添加一个简单的注释,覆盖报告中将忽略该行。

if __name__ == '__main__':
    main()  # pragma: no cover

https://coverage.readthedocs.io/en/coverage-4.3.3/ exclude.html

@Taylor Edmiston 的另一条评论也提到了它

要在 pytest 中导入您的“主要”代码以便对其进行测试,您可以像导入其他函数一样导入主要模块,这要归功于本机 importlib package:

def test_main():
    import importlib
    loader = importlib.machinery.SourceFileLoader("__main__", "src/glue_jobs/move_data_with_resource_partitionning.py")
    runpy_main = loader.load_module()
    assert runpy_main()

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM