1

I. Vorwort: Die Anwendungsverzeichnisstruktur und Module sind am Ende des Beitrags aufgeführt.Python Run Unittest als Paket Import Fehler

II. Problembeschreibung: Wenn PYTHONPATH nicht festgelegt ist, wird die Anwendung ausgeführt, aber der Komponententest schlägt fehl ImportError: Kein Modul namens models.transactions. Dies geschieht, wenn Sie versuchen, Transactions in app.py zu importieren. Wenn der PYTHONPATH auf /sandbox/app eingestellt ist, werden sowohl die Anwendung als auch der Unit Test ohne Fehler ausgeführt. Die Einschränkungen für eine Lösung bestehen darin, dass der PYTHONPATH nicht festgelegt werden sollte, und der sys.path sollte nicht programmgesteuert geändert werden müssen.

III. Details: Betrachten Sie den Fall, wenn der PYTHONPATH eingestellt ist und test_app.py als Paket /sandbox$ python -m unittest tests.test_app ausgeführt wird. Mit Blick auf den Druckanweisungen für __main__ über den gesamten Code gestreut:

models : app.models.transactions 
models : models.transactions 
resources: app.resources.transactions 
app  : app.app 
test  : tests.test_app 

Der Unittest die App importiert zuerst, und so ist es app.models.transactions. Der nächste Import, den die App versucht, ist resources.transactions. Wenn es importiert wird, macht es seinen eigenen Import von models.transactions und dann sehen wir die __name__ für app.resources.transactions. Danach folgt der app.app Import und schließlich das Unittest-Modul tests.test.app. Wenn Sie PYTHONPATH einstellen, kann die Anwendung models.transactions auflösen!

Eine Lösung ist die models.transactions innerhalb resources.transaction zu setzen. Aber gibt es einen anderen Weg, mit dem Problem umzugehen?

Für Vollständigkeit, wenn die Anwendung der Druck ausgeführt wird Anweisungen für __main__ sind:

models : models.transactions 
resources: resources.transactions 
app  : __main__ 

Dies ist zu erwarten, und keine Importe versucht werden, die oben /sandbox/app oder seitlich sind.

IV. Appendix

A.1 Verzeichnisstruktur:

|-- sandbox 
    |-- app 
     |-- models 
      |-- __init__.py 
      |-- transactions.py 
     |-- resources 
      |-- __init__.py 
      |-- transactions.py   
     |-- __init__.py 
     |-- app.py 
    |-- tests 
     |-- __init__.py 
     |-- test_app.py 

A.2 Module:

(1) APP:

from flask import Flask 
from models.transactions import TransactionsModel 
from resources.transactions import Transactions 
print '  app  : ', __name__ 
def create_app(): 
    app = Flask(__name__) 
    return app 
app = create_app() 
if __name__ == '__main__': 
    app.run(host='127.0.0.1', port=5000, debug=True) 

(2) models.transactions

print '  model : ', __name__ 
class TransactionsModel: 
    pass 

(3) resources.transactions:

from models.transactions import TransactionsModel 
print '  resources: ', __name__ 
class Transactions: 
    pass 

(4) Tests.test_app

import unittest 
from app.app import create_app 
from app.resources.transactions import Transactions 
print '  test  : ', __name__ 
class DonationTestCase(unittest.TestCase): 
    def setUp(self): 
     pass 
    def tearDown(self): 
     pass 
    def test_transactions_get_with_none_ids(self): 
     self.assertEqual(0, 0) 
if __name__ == '__main__': 
    unittest.main() 

Antwort

1

Es ist erwähnenswert, dass die Flask-Dokumente die Anwendung als Paket ausführen und die Umgebungsvariable FLASK_APP festlegen. Die Anwendung läuft dann vom Projektstamm: $ python -m flask run. Jetzt enthalten die Importe das Anwendungsstammverzeichnis, z. B. app.models.transactions. Da der Unittest auf die gleiche Weise wie ein Paket aus dem Projektstamm ausgeführt wird, werden dort auch alle Importe aufgelöst.

Der Kern des Problems kann auf folgende Weise beschrieben werden. Die test_app.py benötigt Zugriff auf seitwärts Importe, aber wenn es läuft als Skript wie:

/sandbox/test$ python test_app.py 

es __name__ == __main__ hat. Dies bedeutet, dass Importe wie from models.transactions import TransactionsModel nicht aufgelöst werden, weil sie seitwärts und nicht niedriger in der Hierarchie sind. Um dies zu umgehen kann die test_app.py als Paket ausgeführt werden:

/sandbox$ python unittest -m test.test_app 

Die -m Schalter ist, was Python sagt, dies zu tun. Jetzt hat das Paket Zugriff auf app.model, da es in /sandbox ausgeführt wird. Die Einfuhren in test_app.py müssen diese Änderung widerspiegeln und wie etwas geworden:

from app.models.transactions import TransactionsModel 

Um den Testlauf zu machen, müssen die Einfuhren in der Anwendung jetzt relativ sein. Zum Beispiel in app.resources:

from ..models.transactions import TransactionsModel 

So laufen die Tests erfolgreich, aber wenn die Anwendung ausgeführt wird es nicht! Dies ist der Kern des Problems. Wenn die Anwendung als Skript von /sandbox/app$ python app.py ausgeführt wird, trifft sie auf diesen relativen Import ..models.transactions und gibt einen Fehler zurück, den das Programm über die oberste Ebene zu importieren versucht. Repariere eins und brich das andere.

Wie kann man das umgehen, ohne den PYTHONPATH einstellen zu müssen? Eine mögliche Lösung besteht darin, eine Bedingung in dem Paket __init__ .py für bedingte Importe zu verwenden. Ein Beispiel, wie das aussieht für das resources Paket ist:

if __name__ == 'resources': 
    from models.transactions import TransactionsModel 
    from controllers.transactions import get_transactions 
elif __name__ == 'app.resources': 
    from ..models.transactions import TransactionsModel 
    from ..controllers.transactions import get_transactions 

Das letzte Hindernis zu überwinden ist, wie bekommen wir diese in die resources.py gezogen. Die in __init__ .py erfolgten Importe sind an diese Datei gebunden und stehen resources.py nicht zur Verfügung. Normalerweise würde man den folgenden Import in resources.py umfasst:

import resources 

Aber wieder ist es resources oder app.resources? Es scheint, als ob die Schwierigkeit sich für uns weiter nach unten verschoben hat. Die Werkzeuge angeboten von importlib kann hier helfen, zum Beispiel die Folgenden wird die richtige Import machen:

from importlib import import_module 
import_module(__name__) 

Es gibt andere Methoden, die verwendet werden können. Beispiel:

TransactionsModel = getattr(import_module(__name__), 'TransactionsModel') 

Dies behebt den Fehler im vorliegenden Kontext.

Eine andere, direktere Lösung verwendet absolute Importe in den Modulen selbst. Zum Beispiel in Ressourcen:

models_root = os.path.join(os.path.dirname(__file__), '..', 'models') 
fp, file_path, desc = imp.find_module(module_name, [models_root]) 
TransactionsModel = imp.load_module(module_name, fp, file_path, 
    desc).TransactionsModel 
TransactionType = imp.load_module(module_name, fp, file_path, 
    desc).TransactionType 

Nur eine Anmerkung über die PYTHONPATH mit sys.path.append(app_root) in resources.py ändern. Dies funktioniert gut und ist ein paar Zeilen Code, wo es sein muss. Darüber hinaus ändert es nur den Pfad für die ausführende Datei und kehrt nach Abschluss zurück. Scheint wie ein guter Anwendungsfall für Unittest. Ein Problem könnte sein, wenn die Anwendung in andere Umgebungen verschoben wird.

+0

Die Tatsache, dass "es funktioniert" sollte gegen die Tatsache gewichtet werden, dass dieser Code nicht sehr "lesbar" ist: Mit anderen Worten, andere Leute, die mit der Pflege des Codes beauftragt sind, würden wirklich Schwierigkeiten haben, alles zu verstehen, was Sie hier tun (v. einfach "Wrapper" -Shellskripte bereitstellen, die "das Richtige" tun. Auch wenn Sie weitere Tests/Module hinzufügen, habe ich den Eindruck, dass dies schnell ziemlich unhandlich werden würde. – Marco

+0

Absolut! Nicht die Art und Weise, mit Importen umzugehen, es sei denn, es gibt Einschränkungen, wo man den Python-Pfad sys.path usw. nicht ändern darf. Am Ende, wie ich im ersten Absatz erwähnt habe, besteht die Möglichkeit, FLASK_APP zu setzen , und führen Sie sowohl die App als auch den Unittest als Pakete aus. – Aaron

0

TL; DR: Wenn Sie können, sollten Sie die beiden Importe ändern:

from models.transactions import TransactionsModel 
from resources.transactions import Transactions 

zu

from app.models.transactions import TransactionsModel 
from app.resources.transactions import Transactions 

Längere Version

Wenn Sie sagen:

If the PYTHONPATH is not set the application runs...

Wie starten Sie die App? Ich vermute, so etwas wie ...

cd sandbox/app 
python app.py 

weil entweder die Importe in app.py falsch sind (die meisten Top-app Modul fehlt) oder auch laufen die app sollte ebenso scheitern.

IMO auch Sie müssen nicht (stricly) machen tests ein Modul (dh, können Sie tests/__init__.py fallen), und führen nur die Tests wie:

python tests/test_app.py 

Der springende Punkt ist, dass . (Ihre aktuelle Verzeichnis) ist standardmäßig immer in PYTHONPATH enthielt und es ist dort aus, dass die Module geladen werden/nach Importen - in diesem Fall, wenn die Tests from app.app import create_app in app.py die ersten Zeile ausführen:

from models.transactions import TransactionsModel 

wird den Fehler verursachen (es gibt kein ./models Modul/Verzeichnis).

Wenn Sie nicht die Einfuhren in der app.py Anwendungsmodul ändern können, oder auf andere Weise eingeschränkt sind, die einzige Option, die ich von (anderen als PYTHONPATH Modifizieren oder Manipulieren sys.path, die Sie ausdrücklich ausgeschlossen) denken kann, die nur würde andere Option sein:

cd sandbox/app 
python ../tests/test_app.py 

aber dann würden Sie ändern müssen, in die Komponententests, Ihre Importe:

from app import create_app 
from resources.transactions import Transactions 

mit anderen Worten, Sie nicht Ihren Kuchen haben und essen :) ohne den Python-Lookup-Pfad zu ändern, müssen alle Module von derselben Stelle (.) starten und somit konsistent sein.

+0

Die Anwendung wird als Skript, nicht als Paket ausgeführt, und dies geschieht in/sandbox/app als python app.py. Die Verwendung von Importen wie app.models.transactions wird nicht aufgelöst. Ich habe das versucht. Der zweite Punkt ist, dass test_app als Paket aus/sandbox ausgeführt wird, wie zB python -m unittest test.test_app. Dies ermöglicht den Tests den Zugriff auf Seitwärtsimporte und ist eine gebräuchliche Art, diese Tests auszuführen. Dies funktioniert bis auf die verketteten Importe. – Aaron

+0

"Laufen als Skript" v. "Läuft als Paket" (vorausgesetzt, das letztere ist sogar sinnvoll) ist eine Unterscheidung ohne Unterschied :) Was passiert, wenn Sie Ihre Importe in Ihren Tests wie angegeben ändern, dann führen Sie Ihre Tests aus '/ sandbox/app' mit' python ../ tests/test_app.py' Würde das funktionieren? – Marco

+0

Der Punkt, den ich versuche zu machen, ist, dass innerhalb der Beschränkungen, die Sie haben, mit dem Pfadsystem herumzumischen, Module/Pakete von '.' gelöst werden müssen: Hier versucht Python, Importe von (zusätzlich zu , natürlich, die lib-Ordner der Umgebung und was auch immer sonst schon auf '$ PYTHONPATH' oder' sys.path' steht. – Marco