Как протестировать или издеваться над содержимым "if __name__ == '__main__"

Скажем, у меня есть модуль со следующим:

def main():
    pass

if __name__ == "__main__":
    main()

Я хочу написать unit test для нижней половины (я хотел бы получить 100% -ый охват). Я обнаружил встроенный модуль runpy, который выполняет механизм импорта /__name__ -setting, но я не могу понять, как насмехаться или иным образом проверять, что вызывается функция 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()

Ответ 1

Я выберу другую альтернативу, которая заключается в том, чтобы исключить if __name__ == '__main__' из отчета о покрытии, конечно, вы можете сделать это, только если у вас уже есть тестовый пример для функции main() в ваших тестах.

Что касается того, почему я предпочитаю исключать, а не писать новый тестовый пример для всего сценария, так это потому, что, как я уже говорил, у вас уже есть тестовый пример для вашей функции main(), факт, что вы добавляете другой тестовый пример для сценария (просто за 100% покрытие) будет просто дублированным.

О том, как исключить if __name__ == '__main__', вы можете написать файл конфигурации покрытия и добавить в отчет раздела:

[report]

exclude_lines =
    if __name__ == .__main__.:

Подробнее о файле конфигурации покрытия можно найти здесь.

Надеюсь, это поможет.

Ответ 2

Вы можете сделать это, используя модуль imp, а не оператор import. Проблема с оператором import заключается в том, что тест для '__main__' выполняется как часть оператора импорта, прежде чем вы получите возможность назначить runpy.__name__.

Например, вы можете использовать imp.load_source() следующим образом:

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

Первый параметр присваивается __name__ импортированного модуля.

Ответ 3

Эй, я немного опаздываю на вечеринку, но недавно я столкнулся с этой проблемой, и я думаю, что придумал лучшее решение, так что вот оно...

Я работал над модулем, содержащим дюжину скриптов, заканчивающихся этой точной копией:

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

а затем поместите этот камень в конец каждого файла script:

run_script(__name__, __doc__, main)

Технически эта функция будет выполняться безоговорочно, был ли ваш script импортирован как модуль или запущен как script. Это нормально, потому что функция фактически ничего не делает, если script не запускается как script. Таким образом, покрытие кода видит, что функция запускается и говорит "да, 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)

Blam! Теперь вы можете написать тестовый main(), вызывать его как script, иметь 100% -ное покрытие для тестирования, и не нужно игнорировать какой-либо код в своем отчете о покрытии.

Ответ 4

Один из подходов - запустить модули как скрипты (например, os.system(...)) и сравнить их вывод stdout и stderr с ожидаемыми значениями.

Ответ 5

Мое решение состоит в том, чтобы использовать 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')