So testen oder verspotten Sie den Inhalt von "if __name__ == '__main__'"

72

Angenommen, ich habe ein Modul mit folgenden Eigenschaften:

def main():
    pass

if __name__ == "__main__":
    main()

Ich möchte einen Unit-Test für die untere Hälfte schreiben (ich möchte eine 100% ige Abdeckung erreichen). Ich habe das eingebaute runpy-Modul entdeckt, das den Import / __name__Setting-Mechanismus ausführt, aber ich kann nicht herausfinden, wie man die main () -Funktion verspottet oder auf andere Weise überprüft .

Folgendes habe ich bisher versucht:

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

Antworten:

59

Ich werde eine andere Alternative wählen, die darin besteht, den if __name__ == '__main__'Bericht aus dem Bericht auszuschließen. Natürlich können Sie dies nur tun, wenn Sie bereits einen Testfall für Ihre main () - Funktion in Ihren Tests haben.

Der Grund, warum ich mich dafür entscheide, einen neuen Testfall für das gesamte Skript auszuschließen, anstatt ihn zu schreiben, liegt main()darin, dass Sie, wie bereits erwähnt, bereits einen Testfall für Ihre Funktion haben, weil Sie einen anderen Testfall für das Skript hinzufügen (nur um einen zu haben) 100% Deckung) wird nur eine doppelte sein.

Um das auszuschließen if __name__ == '__main__', können Sie eine Coverage-Konfigurationsdatei schreiben und im Abschnittsbericht hinzufügen:

[report]

exclude_lines =
    if __name__ == .__main__.:

Weitere Informationen zur Coverage-Konfigurationsdatei finden Sie hier .

Hoffe das kann helfen.

Mouad
quelle
Heya, ich habe eine neue Antwort hinzugefügt, die eine 100% ige Testabdeckung bietet (mit Tests!) Und bei der nichts ignoriert werden muss. Lassen Sie mich wissen, was Sie denken: stackoverflow.com/a/27084447/1423157 Vielen Dank.
Robru
Für diejenigen, die sich fragen: nose-covVerwendet Coverage.py darunter, sodass eine .coveragercDatei mit dem oben genannten Inhalt einwandfrei funktioniert.
Joscha
12
IMHO, auch wenn ich fand es interessant und nützlich, diese Antwort ist nicht wirklich eine Antwort auf die OP geben. Er möchte testen, ob main aufgerufen wird, und diese Prüfung nicht überspringen. Andernfalls könnte das Skript tatsächlich alles tun, außer was beim Start tatsächlich erwartet wurde, wobei Tests "OK, alles funktioniert!" Sagen. Und die Hauptfunktion könnte vollständig auf Einheiten getestet werden, selbst wenn sie niemals aufgerufen wird.
Iacopo
1
Es gibt vielleicht keine Antwort auf OP, aber es ist eine gute Antwort für praktische Zwecke, so habe ich diese Frage zumindest gefunden. Eine ähnliche Lösung wird # pragma: no coverwie folgt verwendet if __name__ == '__main__': # pragma: no cover. Persönlich bin ich nicht bereit, dies zu tun, weil es den Code überfüllt und ziemlich hässlich ist, daher denke ich, dass Mouads Antwort die beste Lösung ist, aber andere mögen es nützlich finden.
Taylor Edmiston
@mouad Wenn wir sehr spezifisch sind, denke ich, dass die Regex-Zeile technisch verwendet werden sollte, ['"]anstatt .wie folgt : __name__ == ['"]__main__['"]:.
Taylor Edmiston
13

Sie können dies mit dem impModul und nicht mit der importAnweisung tun . Das Problem mit der importAnweisung besteht darin, dass der Test für '__main__'als Teil der Importanweisung ausgeführt wird, bevor Sie die Möglichkeit erhalten, ihn zuzuweisen runpy.__name__.

Zum Beispiel könnten Sie imp.load_source()so verwenden:

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

Der erste Parameter wird dem __name__importierten Modul zugewiesen .

David Heffernan
quelle
6
Das imp-Modul scheint ähnlich zu funktionieren wie das Runpy-Modul, das ich in der Frage verwendet habe. Das Problem ist, dass der Mock (anscheinend) nicht eingefügt werden kann, nachdem das Modul geladen wurde und bevor der Code ausgeführt wurde. Haben Sie Vorschläge dazu?
Nikolaj
7

Whoa, ich bin ein bisschen zu spät zur Party, aber ich bin kürzlich auf dieses Problem gestoßen und ich denke, ich habe eine bessere Lösung gefunden, also hier ist es ...

Ich habe an einem Modul gearbeitet, das ungefähr ein Dutzend Skripte enthielt, die alle mit genau dieser Copypasta endeten:

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

Sicher nicht schrecklich, aber auch nicht testbar. Meine Lösung bestand darin, eine neue Funktion in eines meiner Module zu schreiben:

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

und platzieren Sie dieses Juwel am Ende jeder Skriptdatei:

run_script(__name__, __doc__, main)

Technisch gesehen wird diese Funktion bedingungslos ausgeführt, unabhängig davon, ob Ihr Skript als Modul importiert oder als Skript ausgeführt wurde. Dies ist jedoch in Ordnung, da die Funktion nur dann etwas tut , wenn das Skript als Skript ausgeführt wird. Die Codeabdeckung sieht also, dass die Funktion ausgeführt wird und sagt "Ja, 100% Codeabdeckung!" In der Zwischenzeit habe ich drei Tests geschrieben, um die Funktion selbst abzudecken:

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

Schuld! Jetzt können main()Sie ein Testobjekt schreiben , es als Skript aufrufen, eine 100% ige Testabdeckung haben und müssen keinen Code in Ihrem Abdeckungsbericht ignorieren.

Robru
quelle
22
Ich schätze die Kreativität und Ausdauer bei der Suche nach einer Lösung, aber wenn Sie in meinem Team wären, würde ich diese Art der Codierung ablehnen. Eine der Stärken von Python ist seine hohe Redewendung. if __name__ == ...ist der Weg, um ein Modul Skript zu lassen. Jeder Pythonist erkennt diese Zeile und versteht, was sie tut. Ihre Lösung ist es, das Offensichtliche nur aus einem guten Grund zu verschleiern, außer einen intellektuellen Juckreiz zu kratzen. Wie gesagt: Eine clevere Lösung, aber clever ist nicht immer gleichbedeutend mit Korrektur .
Mac
Das ist in Ordnung, wenn Sie nur ein Modul haben oder wenn jedes Modul etwas anderes tut, wenn es als Skript aufgerufen wird, aber wie gesagt, ich hatte am Ende ein Dutzend Dateien mit völlig identischen if __name__ == ... Blöcken, was eine große Verletzung von Don't Repeat Yourself darstellt und macht es auch schwierig, Fehler zu beheben, wenn Sie sie an so vielen Stellen identisch beheben müssen. Eine solche Vereinheitlichung der Logik erhöht die Testbarkeit und verringert das Fehlerpotential. Wenn Sie sich Sorgen machen, dass die Leute es nicht verstehen, benennen Sie die Funktion if_name_equals_main()und die Leute werden es herausfinden.
Robru
8
Wenn Sie eine Logik in dem Block haben, unter dem eingerückt if __name__ ...ist, machen Sie es falsch und sollten umgestalten. Die einzige Codezeile unter if __name__...sollte lauten : main().
Mac
1
@mac Ich weiß nicht, dass ich damit einverstanden bin. Ja, wenn Sie Logik haben , sollten Sie umgestalten. Das heißt aber nicht, dass das einzige, was Sie unter haben können, if __name__ ...ist main(). Zum Beispiel verwende ich gerne argeparse und konstruiere meinen Parser in dem if __name__ ...Teil. Dann abstrahiere mein Haupt, um explizite Argumente zu verwenden, anstatt so etwas wie : main(parser.parse_args()). Dies erleichtert das Aufrufen main()von einem anderen Modul bei Bedarf. Andernfalls müssen Sie ein argeparse.Namespace()Objekt erstellen und alle Standardargumente korrekt ausführen. Oder gibt es einen idiomatischeren Weg, dies zu tun?
Michael Leonard
@ MichaelLeonard - Ich bin nicht sicher, ob ich Ihre Frage richtig verstehe. mainist - gemäß Konvention - die Funktion, die ausgeführt werden soll, wenn das Modul als Skript aufgerufen wird. Daher ist dies der herkömmliche Ort, an dem der Parsing-Code ausgeführt werden soll. Wenn Sie eine einzelne Funktion haben, die Sie innerhalb des Moduls verfügbar machen möchten, sollte diese nicht aufgerufen werden, mainsondern etwas anderes, und die mainFunktion sollte sie wiederum aufrufen und die analysierten Argumente übergeben. Oder verstehe ich Ihre Frage völlig falsch?
Mac
3

Ein Ansatz besteht darin, die Module als Skripte (z. B. os.system (...)) auszuführen und ihre stdout- und stderr-Ausgabe mit den erwarteten Werten zu vergleichen.

Herr Fooz
quelle
2
Das Ausführen des Skripts in einem Unterprozess und das Erwarten, dass Coverage.py die ausgeführte Zeile verfolgt, ist nicht so einfach, wie es sich anhört.
mouad
2

Python 3-Lösung:

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

Verwenden Sie die alternative Lösung zum Definieren Ihrer eigenen kleinen Funktion:

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

Sie können testen mit:

# 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')
Samuel Marks
quelle
1

Meine Lösung besteht darin imp.load_source(), eine Ausnahme zu verwenden und zu erzwingen, dass sie frühzeitig main()ausgelöst wird, indem kein erforderliches CLI-Argument angegeben wird, ein fehlerhaftes Argument angegeben wird, Pfade so festgelegt werden, dass eine erforderliche Datei nicht gefunden wird usw.

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)

Dann können Sie in Ihrer Testklasse diese Funktion folgendermaßen verwenden:

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

Ich fand diese Lösung hilfreich. Funktioniert gut, wenn Sie eine Funktion verwenden, um Ihren gesamten Skriptcode zu behalten. Der Code wird als eine Codezeile behandelt. Es spielt keine Rolle, ob die gesamte Zeile für den Abdeckungszähler ausgeführt wurde (obwohl dies bei einer 100% igen Abdeckung eigentlich nicht zu erwarten ist). Der Trick wird auch als Pylint akzeptiert. ;-);

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