web-dev-qa-db-fra.com

Comment tester ou simuler le contenu de "if __name__ == '__main__'"

Disons que j'ai un module avec les éléments suivants:

def main():
    pass

if __name__ == "__main__":
    main()

Je veux écrire un test unitaire pour la moitié inférieure (j'aimerais obtenir une couverture à 100%). J'ai découvert le module intégré runpy qui effectue l'importation/__name__- mécanisme de réglage, mais je ne peux pas comprendre comment se moquer ou vérifier autrement que la fonction main () est appelée.

Voici ce que j'ai essayé jusqu'à présent:

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

Je vais choisir une autre alternative qui consiste à exclure le if __== '__main__' Du rapport de couverture, bien sûr vous ne pouvez le faire que si vous avez déjà un cas de test pour votre fonction main () dans vos tests.

La raison pour laquelle j'ai choisi d'exclure plutôt que d'écrire un nouveau scénario de test pour tout le script est que si, comme je l'ai dit, vous disposez déjà d'un scénario de test pour votre fonction main(), le fait que vous ajoutiez un autre scénario de test pour le script (juste pour avoir une couverture à 100%) sera juste un doublon.

Pour savoir comment exclure le if __== '__main__', Vous pouvez écrire un fichier de configuration de couverture et l'ajouter dans le rapport de section:

[report]

exclude_lines =
    if __== .__main__.:

Plus d'informations sur le fichier de configuration de couverture peuvent être trouvées ici .

J'espère que cela peut vous aider.

48
mouad

Vous pouvez le faire en utilisant le module imp plutôt que l'instruction import. Le problème avec l'instruction import est que le test de '__main__' S'exécute dans le cadre de l'instruction d'importation avant que vous n'ayez la possibilité de l'attribuer à runpy.__name__.

Par exemple, vous pouvez utiliser imp.load_source() comme ceci:

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

Le premier paramètre est affecté à __name__ Du module importé.

12
David Heffernan

Whoa, je suis un peu en retard à la fête, mais j'ai récemment rencontré ce problème et je pense avoir trouvé une meilleure solution, alors voici ...

Je travaillais sur un module qui contenait une douzaine de scripts se terminant tous par ce copypasta exact:

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

Pas horrible, bien sûr, mais non testable non plus. Ma solution a été d'écrire une nouvelle fonction dans l'un de mes modules:

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

puis placez ce joyau à la fin de chaque fichier de script:

run_script(__name__, __doc__, main)

Techniquement, cette fonction sera exécutée sans condition, que votre script ait été importé en tant que module ou exécuté en tant que script. Ceci est cependant correct car la fonction ne fait --- faire rien sauf si le script est exécuté en tant que script. Ainsi, la couverture de code voit la fonction s'exécuter et dit "oui, couverture de code à 100%!" Pendant ce temps, j'ai écrit trois tests pour couvrir la fonction elle-même:

@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! Vous pouvez maintenant écrire une main() testable, l'invoquer en tant que script, avoir une couverture de test à 100% et ne pas avoir besoin d'ignorer tout code dans votre rapport de couverture.

6
robru

Une approche consiste à exécuter les modules en tant que scripts (par exemple, os.system (...)) et à comparer leur sortie stdout et stderr aux valeurs attendues.

2
Mr Fooz

Ma solution consiste à utiliser imp.load_source() et à forcer une exception à être levée au début de main() en ne fournissant pas un argument CLI requis, en fournissant un argument mal formé, en définissant des chemins de telle manière qu'un requis le fichier est introuvable, etc.

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)

Ensuite, dans votre classe de test, vous pouvez utiliser cette fonction comme ceci:

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