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


72

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

def main():
    pass

if __name__ == "__main__":
    main()

我想为下半部分编写一个单元测试(我想实现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()

Answers:


59

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

至于为什么我选择排除而不是为整个脚本编写新的测试用例的原因是,因为正如我所说的那样,您已经为您的main()函数准备了一个测试用例,所以您为脚本添加了另一个测试用例(只是为了拥有一个测试用例)。 100%的覆盖率)将只是重复项。

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

[report]

exclude_lines =
    if __name__ == .__main__.:

可以在此处找到有关coverage配置文件的更多信息。

希望这会有所帮助。


嘿,我添加了一个新答案,该答案给出了100%的测试覆盖率(带有测试!),并且不需要忽略任何内容。让我知道您的想法:stackoverflow.com/a/27084447/1423157谢谢。
robru 2014年

对于那些想知道的人:nose-cov在下面使用coverage.py,这样.coveragerc具有上述内容的文件就可以正常工作。
Joscha 2015年

12
恕我直言,即使我发现它有趣且有用,这个答案实际上并没有对OP做出回应。他想测试是否调用了main,而不是跳过此检查。否则,脚本在启动时实际上可以执行除实际期望之外的所有操作,并显示“ OK,一切正常!”的测试。即使从未真正调用过main函数,也可以对其进行完全的单元测试。
iacopo '02

1
它可能不会对OP做出回应,但出于实际目的,这是一个很好的答案,这至少是我找到此问题的方式。类似的解决方案是# pragma: no cover像这样使用if __name__ == '__main__': # pragma: no cover。我个人不愿意这样做,因为它会使代码混乱并且非常丑陋,因此我认为mouad的答案是最好的解决方案,但其他人可能会觉得有用。
泰勒·埃德米斯顿

@mouad如果我们非常具体,我认为从技术上讲,正则表达式行应使用['"]而不是.like __name__ == ['"]__main__['"]:
泰勒·埃德米斯顿

13

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

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

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

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


6
imp模块的工作方式与我在问题中使用的runpy模块非常相似。问题在于,无法(显然)在模块加载之后和代码运行之前插入模拟。您对此有什么建议吗?
Nikolaj

7

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

我正在开发一个包含十几个脚本的模块,这些脚本都以这个完全相同的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)

lam!现在,您可以编写一个testable main(),将其作为脚本调用,具有100%的测试覆盖率,并且无需忽略覆盖率报告中的任何代码。


22
感谢您在寻找解决方案中的创造力和毅力,但是如果您在我的团队中,我会否决这种编码方式。Python的优势之一是其高度惯用。 if __name__ == ...,让一个模块脚本的方式。任何pythonista都将识别该行并了解其作用。您的解决方案只是无缘无故地混淆了显而易见的内容,只不过是ing了理智的痒。就像我说的:聪明的解决方案,但聪明并不总是等于纠正
mac

如果您只有一个模块,或者每个模块在作为脚本调用时确实有所不同,那很好,但是正如我所说的,我有十几个文件具有完全相同的 if __name__ == ...块,这严重违反了“不要重复自己”当您需要在许多地方以同样的方式修复错误时,也很难修复错误。统一这样的逻辑可以提高可测试性,并减少潜在的错误。如果您担心别人不了解它,请给函数命名,if_name_equals_main()然后人们就会弄清楚它。
robru

8
如果在缩进的块中有任何逻辑if __name__ ...那么您做错了,应该重构。下面的唯一代码行if __name__...应为:main()
mac

1
@mac我不知道我同意这一点。是的,如果您有逻辑,则应该重构。但这并不意味着您唯一可以拥有的if __name__ ...就是main()。例如,我喜欢使用argeparse并在该if __name__ ...部分中构造解析器。然后抽象我的main以使用显式args而不是像:main(parser.parse_args())main()如果需要,这使得从另一个模块调用更加容易。否则,您必须构造一个argeparse.Namespace()对象并正确获取所有默认args。还是有一种更惯用的方式来做到这一点?
Michael Leonard

@MichaelLeonard-我不确定我是否正确理解您的问题。 main-按惯例-是作为脚本调用模块时应运行的功能,因此它是解析代码的常规位置。如果您要从模块内部公开单个函数main,则main不应调用该函数,而应调用其他函数,并且该函数应依次调用它传递已解析的参数。还是我完全误解了您的问题?
mac

3

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


2
在子过程中运行脚本并期望coverage.py跟踪执行的行并不是一件容易的事,可在以下位置找到使该解决方案起作用的更多信息:nedbatchelder.com/code/coverage/subprocess.html
mouad

2

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

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

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

1

我的解决方案是通过不提供必需的CLI参数,提供格式错误的参数,以无法找到所需文件的方式设置路径等方式使用imp.load_source()并强制提早引发异常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)

然后,在测试类中,您可以使用以下功能:

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

0

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

if __name__ == '__main__': \
    main()
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.