Как проверить или смоделировать содержимое "if __name__ == '__main__'" - PullRequest
60 голосов
/ 01 мая 2011

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

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

Ответы [ 5 ]

46 голосов
/ 01 мая 2011

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

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

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

[report]

exclude_lines =
    if __name__ == .__main__.:

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

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

12 голосов
/ 01 мая 2011

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

Например, вы можете использовать imp.load_source()например:

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

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

6 голосов
/ 23 ноября 2014

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

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

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

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

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 голосов
/ 01 мая 2011

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

0 голосов
/ 30 мая 2018

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