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()
quelle
nose-cov
Verwendet Coverage.py darunter, sodass eine.coveragerc
Datei mit dem oben genannten Inhalt einwandfrei funktioniert.# pragma: no cover
wie folgt verwendetif __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.['"]
anstatt.
wie folgt :__name__ == ['"]__main__['"]:
.Sie können dies mit dem
imp
Modul und nicht mit derimport
Anweisung tun . Das Problem mit derimport
Anweisung besteht darin, dass der Test für'__main__'
als Teil der Importanweisung ausgeführt wird, bevor Sie die Möglichkeit erhalten, ihn zuzuweisenrunpy.__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 .quelle
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:
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.quelle
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 .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 Funktionif_name_equals_main()
und die Leute werden es herausfinden.if __name__ ...
ist, machen Sie es falsch und sollten umgestalten. Die einzige Codezeile unterif __name__...
sollte lauten :main()
.if __name__ ...
istmain()
. Zum Beispiel verwende ich gerne argeparse und konstruiere meinen Parser in demif __name__ ...
Teil. Dann abstrahiere mein Haupt, um explizite Argumente zu verwenden, anstatt so etwas wie :main(parser.parse_args())
. Dies erleichtert das Aufrufenmain()
von einem anderen Modul bei Bedarf. Andernfalls müssen Sie einargeparse.Namespace()
Objekt erstellen und alle Standardargumente korrekt ausführen. Oder gibt es einen idiomatischeren Weg, dies zu tun?main
ist - 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,main
sondern etwas anderes, und diemain
Funktion sollte sie wiederum aufrufen und die analysierten Argumente übergeben. Oder verstehe ich Ihre Frage völlig falsch?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.
quelle
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')
quelle
Meine Lösung besteht darin
imp.load_source()
, eine Ausnahme zu verwenden und zu erzwingen, dass sie frühzeitigmain()
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')
quelle
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()
quelle