Python-Test mit Pytest. Verwenden von pytest mit anderen Tools, KAPITEL 7

Zurück


In der Regel wird pytest nicht unabhängig verwendet, sondern in einer Testumgebung mit anderen Tools. In diesem Kapitel werden andere Tools erläutert, die häufig in Verbindung mit pytest für effektive und effiziente Tests verwendet werden. Obwohl dies keine vollständige Liste ist, geben Ihnen die hier beschriebenen Werkzeuge einen Eindruck vom Geschmack der Fähigkeit, Pytest mit anderen Werkzeugen zu mischen.



Die Beispiele in diesem Buch wurden mit Python 3.6 und pytest 3.2 geschrieben. pytest 3.2 unterstützt Python 2.6, 2.7 und Python 3.3+.


Der Quellcode für das Aufgabenprojekt sowie für alle in diesem Buch gezeigten Tests ist unter dem Link auf der Webseite des Buches unter pragprog.com verfügbar . Sie müssen den Quellcode nicht herunterladen, um den Testcode zu verstehen. Der Testcode wird in den Beispielen in einer praktischen Form dargestellt. Um jedoch die Aufgaben des Projekts zu verfolgen oder Testbeispiele anzupassen, um Ihr eigenes Projekt zu testen (Ihre Hände sind losgebunden!), Müssen Sie auf die Webseite des Buches gehen und die Arbeit herunterladen. Dort, auf der Webseite des Buches, gibt es einen Link für Errata- Nachrichten und ein Diskussionsforum .

Unter dem Spoiler befindet sich eine Liste der Artikel dieser Reihe.



pdb: Debuggen von Testfehlern


Das pdb Modul ist ein Python-Debugger in der Standardbibliothek. Sie verwenden --pdb damit pytest an der Fehlerstelle eine Debugging-Sitzung startet. Schauen wir uns pdb in Aktion im Kontext des Aufgabenprojekts an.


In „Parametrisierung der Vorrichtung“ auf Seite 64 haben wir das Aufgabenprojekt mit einigen Fehlern verlassen:


 $ cd /path/to/code/ch3/c/tasks_proj $ pytest --tb=no -q .........................................FF.FFFF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.FFF........... 42 failed, 54 passed in 4.74 seconds 

Bevor wir uns ansehen, wie pdb uns beim Debuggen dieses Tests helfen kann, pdb wir einen Blick auf die verfügbaren Pytest-Optionen, um das Debuggen der Testfehler zu beschleunigen, die wir zuerst im Abschnitt „Verwenden von Optionen“ auf Seite 9 untersucht haben:


  • --tb=[auto/long/short/line/native/no] : Steuert den Trace-Stil.
  • -v / --verbose : Zeigt alle -v / --verbose , die bestanden oder nicht bestanden wurden.
  • -l / --showlocals : Zeigt lokale Variablen neben dem Stack-Trace an.
  • -lf / --last-failed : -lf / --last-failed nur -lf / --last-failed Tests aus.
  • -x / --exitfirst : Stoppt die -x / --exitfirst beim ersten Fehler.
  • --pdb : Startet eine interaktive Debugging-Sitzung zum Zeitpunkt des Fehlers.



MongoDB installieren




Wie in Kapitel 3, „Pytest-Vorrichtungen“, auf Seite 49 erwähnt, sind pymongo und pymongo Installationen erforderlich, um MongoDB-Tests durchzuführen.


Ich habe die Version von Community Server unter https://www.mongodb.com/download-center getestet. pymongo installiert mit pip : pip install pymongo . Dies ist jedoch das letzte Beispiel in einem Buch, das MongoDB verwendet. Um den Debugger ohne Verwendung von MongoDB auszuprobieren, können Sie die pytest-Befehle über den code/ch2/ , da dieses Verzeichnis auch mehrere fehlgeschlagene Tests enthält.




Wir haben gerade die Tests mit code/ch3/c , um sicherzustellen, dass einige davon nicht funktionieren. Wir haben keine Tracebacks oder --tb=no da --tb=no die Ablaufverfolgung deaktiviert und --verbose aktiviert hat. Wiederholen wir die Fehler (nicht mehr als drei) mit dem detaillierten Text:


 $ pytest --tb=no --verbose --lf --maxfail=3 ============================= test session starts ============================= collected 96 items / 52 deselected run-last-failure: rerun previous 44 failures tests/func/test_add.py::test_add_returns_valid_id[mongo] ERROR [ 2%] tests/func/test_add.py::test_added_task_has_id_set[mongo] ERROR [ 4%] tests/func/test_add.py::test_add_increases_count[mongo] ERROR [ 6%] =================== 52 deselected, 3 error in 0.72 seconds ==================== 

Jetzt wissen wir, welche Tests fehlgeschlagen sind. Schauen wir uns nur einen von ihnen an, indem wir -x , die Ablaufverfolgung --tb=no , nicht --tb=no und lokale Variablen mit -l --tb=no :


 $ pytest -v --lf -l -x ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED =========================== FAILURES =========================== _______________ test_add_returns_valid_id[mongo] _______________ tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) > assert isinstance(task_id, int) E AssertionError: assert False E + where False = isinstance(ObjectId('59783baf8204177f24cb1b68'), int) new_task = Task(summary='do something', owner=None, done=False, id=None) task_id = ObjectId('59783baf8204177f24cb1b68') tasks_db = None tests/func/test_add.py:16: AssertionError !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== =========== 1 failed, 54 deselected in 2.47 seconds ============ 

Sehr oft reicht dies aus, um zu verstehen, warum der Test fehlgeschlagen ist. In diesem speziellen Fall ist es ziemlich klar, dass task_id keine Ganzzahl ist - es ist eine Instanz von ObjectId. ObjectId ist der Typ, der von MongoDB für Objektkennungen in der Datenbank verwendet wird. Meine Absicht mit der tasksdb_pymongo.py Ebene war es, bestimmte Details der MongoDB-Implementierung vor dem Rest des Systems zu verbergen. Es ist klar, dass es in diesem Fall nicht funktioniert hat.


Wir möchten jedoch sehen, wie pdb mit pytest verwendet wird. Stellen wir uns also vor, es ist unklar, warum dieser Test fehlgeschlagen ist. Mit --pdb können wir pytest veranlassen, eine Debugging-Sitzung zu starten und uns direkt am Fehlerpunkt zu starten:


 $ pytest -v --lf -x --pdb ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED >>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>> tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) > assert isinstance(task_id, int) E AssertionError: assert False E + where False = isinstance(ObjectId('59783bf48204177f2a786893'), int) tests/func/test_add.py:16: AssertionError >>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>> > /path/to/code/ch3/c/tasks_proj/tests/func/test_add.py(16) > test_add_returns_valid_id() -> assert isinstance(task_id, int) (Pdb) 

Jetzt, da wir an der Eingabeaufforderung (Pdb) sind, haben wir Zugriff auf alle interaktiven PDF-Debugging-Funktionen. Beim Anzeigen von Abstürzen verwende ich regelmäßig die folgenden Befehle:


  • p/print expr : p/print expr den Wert von exp.
  • pp expr : Pretty druckt den Wert von expr.
  • l/list : Listet den Fehlerpunkt und fünf Codezeilen oben und unten auf.
  • l/list begin,end : Listet bestimmte Zeilennummern auf.
  • a/args : Druckt die Argumente der aktuellen Funktion mit ihren Werten.
  • u/up : Verschiebt den Stapelpfad um eine Ebene nach oben.
  • d/down : Verschiebt sich im Stack-Trace um eine Ebene nach unten.
  • q/quit : Beendet eine Debugging-Sitzung.

Andere Navigationsbefehle wie step und next sind nicht sehr nützlich, da wir direkt in der assert-Anweisung sitzen. Sie können auch einfach Variablennamen eingeben und Werte abrufen.


Sie können p/print expr ähnlich wie die -l/--showlocals , um die Werte in einer Funktion -l/--showlocals :


 (Pdb) p new_task Task(summary='do something', owner=None, done=False, id=None) (Pdb) p task_id ObjectId('59783bf48204177f2a786893') (Pdb) 

Jetzt können Sie den Debugger beenden und mit dem Testen fortfahren.


 (Pdb) q !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== ========== 1 failed, 54 deselected in 123.40 seconds =========== 

Wenn wir - nicht verwenden würden, würde pytest Pdb im nächsten Test erneut öffnen. Weitere Informationen zur Verwendung des pdb-Moduls finden Sie in der Python-Dokumentation .


Coverage.py: Bestimmen der Menge an Testcode


Die Codeabdeckung ist ein Indikator für den Prozentsatz des getesteten Codes, der durch eine Reihe von Tests getestet wird. Wenn Sie Tests für das Aufgabenprojekt ausführen, werden einige Aufgabenfunktionen mit jedem Test ausgeführt, jedoch nicht mit allen.


Tools zur Codeabdeckung eignen sich hervorragend, um Sie darüber zu informieren, welche Teile des Systems bei Tests vollständig übersehen werden.


Coverage.py ist das bevorzugte Python-Coverage-Tool, das die Codeabdeckung misst.


Sie werden es verwenden, um den Aufgabenprojektcode mit pytest zu überprüfen.


Um coverage.py , müssen Sie es installieren. Es pytest-cov nicht weh, ein Plugin namens pytest-cov zu installieren, mit dem Sie coverage.py von pytest mit einigen zusätzlichen Pytest-Optionen aufrufen können. Da coverage eine der Abhängigkeiten von pytest-cov , installieren pytest-cov einfach pytest-cov und es wird coverage.py :


 $ pip install pytest-cov Collecting pytest-cov Using cached pytest_cov-2.5.1-py2.py3-none-any.whl Collecting coverage>=3.7.1 (from pytest-cov) Using cached coverage-4.4.1-cp36-cp36m-macosx_10_10_x86 ... Installing collected packages: coverage, pytest-cov Successfully installed coverage-4.4.1 pytest-cov-2.5.1 

Lassen Sie uns den Abdeckungsbericht für die zweite Taskversion ausführen. Wenn Sie immer noch die erste Version des Aufgabenprojekts installiert haben, deinstallieren Sie es und installieren Sie Version 2:


 $ pip uninstall tasks Uninstalling tasks-0.1.0: /path/to/venv/bin/tasks /path/to/venv/lib/python3.6/site-packages/tasks.egg-link Proceed (y/n)? y Successfully uninstalled tasks-0.1.0 $ cd /path/to/code/ch7/tasks_proj_v2 $ pip install -e . Obtaining file:///path/to/code/ch7/tasks_proj_v2 ... Installing collected packages: tasks Running setup.py develop for tasks Successfully installed tasks $ pip list ... tasks (0.1.1, /path/to/code/ch7/tasks_proj_v2/src) ... 

Nachdem die nächste Version der Aufgaben installiert ist, können Sie den Basisabdeckungsbericht ausführen:


 $ cd /path/to/code/ch7/tasks_proj_v2 $ pytest --cov=src ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Name Stmts Miss Cover -------------------------------------------------- src\tasks\__init__.py 2 0 100% src\tasks\api.py 79 22 72% src\tasks\cli.py 45 14 69% src\tasks\config.py 18 12 33% src\tasks\tasksdb_pymongo.py 74 74 0% src\tasks\tasksdb_tinydb.py 32 4 88% -------------------------------------------------- TOTAL 250 126 50% ================== 62 passed in 0.47 seconds =================== 

Da das aktuelle Verzeichnis tasks_proj_v2 und sich der zu tasks_proj_v2 Quellcode in src befindet, wird durch Hinzufügen der Option --cov=src nur für dieses zu --cov=src Verzeichnis ein Abdeckungsbericht generiert.


Wie Sie sehen können, haben einige Dateien eine recht geringe und sogar 0% ige Abdeckung. Dies sind nützliche Erinnerungen: tasksdb_pymongo.py 0%, da wir das Testen auf MongoDB in dieser Version deaktiviert haben. Einige von ihnen sind ziemlich niedrig. Das Projekt muss sicherlich Tests für alle diese Bereiche liefern, bevor es zur Hauptsendezeit bereit ist.


Ich glaube, dass mehrere Dateien einen höheren Prozentsatz der Abdeckung haben: api.py und tasksdb_tinydb.py . tasksdb_tinydb.py wir einen Blick auf tasksdb_tinydb.py und sehen, was fehlt. Ich denke, der beste Weg, dies zu tun, ist die Verwendung von HTML-Berichten.


Wenn Sie coverage.py erneut mit der Option --cov-report=html , wird ein --cov-report=html generiert:


 $ pytest --cov=src --cov-report=html ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Coverage HTML written to dir htmlcov ================== 62 passed in 0.45 seconds =================== 

Sie können dann htmlcov/index.html in einem Browser öffnen, der die Ausgabe auf dem folgenden Bildschirm anzeigt:



Wenn Sie auf tasksdb_tinydb.py wird ein Bericht für eine Datei angezeigt. Der Prozentsatz der abgedeckten Zeilen wird oben im Bericht angezeigt, plus wie viele Zeilen abgedeckt sind und wie viele nicht, wie auf dem nächsten Bildschirm angezeigt:



Wenn Sie nach unten scrollen, sehen Sie die fehlenden Linien, wie im folgenden Bildschirm gezeigt:



Auch wenn dieser Bildschirm keine vollständige Seite für diese Datei ist, reicht dies aus, um uns Folgendes mitzuteilen:


  1. Wir testen list_tasks() mit dem Eigentümer-Set.
  2. Wir testen update() oder delete() .
  3. Vielleicht testen wir unique_id() nicht gründlich.

Großartig. Wir können sie zusammen mit dem Testen des Konfigurationssystems in unsere TO-DO-Testliste aufnehmen.


Obwohl Tools zur Codeabdeckung äußerst nützlich sind, kann das Streben nach 100% iger Abdeckung gefährlich sein. Wenn Sie Code sehen, der nicht getestet wird, kann dies bedeuten, dass ein Test erforderlich ist. Es kann aber auch bedeuten, dass einige Systemfunktionen nicht benötigt werden und entfernt werden können. Wie alle Softwareentwicklungstools ersetzt die Analyse der Codeabdeckung nicht das Denken.


Weitere pytest-cov Sie in der coverage.py und pytest-cov .


mock: Ersatz von Systemteilen


Das Scheinpaket wird verwendet, um Teile des Systems zu ersetzen und Teile des Testcodes vom Rest des Systems zu isolieren. Scheinobjekte werden manchmal als Testdoppel, Spione, Fälschungen oder Stummel bezeichnet.


Zwischen Ihrem eigenen Pytest-Monkeypatch-Gerät (beschrieben unter Verwenden von Monkeypatch auf Seite 85) und Mock sollten Sie über alle erforderlichen Dual-Test-Funktionen verfügen.


Achtung! Verspottet und sehr komisch
Wenn Sie zum ersten Mal auf Testzwillinge wie Mocks, Stubs und Spione treffen, machen Sie sich bereit! Es wird sehr seltsam sein, sehr schnell, lustig, wenn auch sehr beeindruckend.

Das mock Paket enthält die Standard-Python-Bibliothek wie unittest.mock seit Python 3.3. In früheren Versionen ist es als separates Paket verfügbar, das über PyPI installiert wird. Dies bedeutet, dass Sie die nachgebildete PyPI-Version von Python 2.6 bis zur neuesten Python-Version verwenden und die gleiche Funktionalität wie die neueste nachgebildete Python-Version erhalten können. Für die Verwendung mit pytest verfügt ein Plugin namens pytest-mock über einige Funktionen, die es zu meiner bevorzugten Schnittstelle für das Mock-System machen.


Für das Aufgabenprojekt verwenden wir mock , um die Befehlszeilenschnittstelle zu testen. In Coverage.py: cli.py Sie ermittelt haben, wie viel Code getestet wird, haben Sie auf Seite 129 cli.py dass unsere Datei cli.py nicht getestet wurde. Wir werden jetzt anfangen, das Problem zu beheben. Aber lassen Sie uns zuerst über Strategie sprechen.


Die erste Lösung im Aufgabenprojekt bestand darin, die meisten Funktionalitätstests über api.py Eine vernünftige Lösung besteht daher darin, dass das Testen der Befehlszeile kein vollständiger Funktionstest sein muss. Wir können sicher sein, dass das System über die CLI funktioniert, wenn wir während des CLI-Tests eine nasse API-Ebene erhalten. Es ist auch eine bequeme Lösung, mit der wir uns Moki in diesem Abschnitt ansehen können.


Die Implementierung von CLI-Aufgaben verwendet ein Click- Befehlszeilenschnittstellenpaket eines Drittanbieters. Es gibt viele Alternativen für die Implementierung der Befehlszeilenschnittstelle, einschließlich eines in Python argparse . Einer der Gründe, warum ich mich für Click entschieden habe, ist, dass es eine Test-Engine enthält, mit der wir Click-Anwendungen testen können. Obwohl wir hoffen, dass der Code in cli.py typisch für Click-Anwendungen ist, ist er nicht offensichtlich.


Lassen Sie uns die dritte Version von Tasks verlangsamen und installieren:


 $ cd /path/to/code/ $ pip install -e ch7/tasks_proj_v2 ... Successfully installed tasks 

Im Rest dieses Abschnitts werden Sie mehrere Tests entwickeln, um die Funktionalität von "Liste" zu testen.
Lassen Sie es uns in Aktion sehen, um zu verstehen, was wir überprüfen werden:


Hinweis für den Übersetzer: Bei Verwendung der Windows-Plattform sind beim Testen der folgenden Sitzung verschiedene Probleme aufgetreten.
  1. Für die Datenbank " tasks_db im Ordner Ihres Benutzers ein Ordner erstellt werden. Zum Beispiel c:\Users\User_1\tasks_db\
    Andernfalls erhalten wir - >> FileNotFoundError: [Errno 2] Keine solche Datei oder kein solches Verzeichnis: 'c: \ Benutzer \ Benutzer_1 // Aufgaben_db // Aufgaben_db.json'
  2. Verwenden Sie doppelte Anführungszeichen anstelle eines Apostrophs. Andernfalls erhalten Sie einen Fehler
    "Mach etwas Großartiges"
    Verwendung: Aufgaben fügen [OPTIONEN] ZUSAMMENFASSUNG hinzu
    Versuchen Sie "Aufgaben add -h" für Hilfe.

    Fehler: Ich habe unerwartete zusätzliche Argumente erhalten (etwas Großartiges)


 $ tasks list ID owner done summary -- ----- ---- ------- $ tasks add 'do something great' $ tasks add "repeat" -o Brian $ tasks add "again and again" --owner Okken $ tasks list ID owner done summary -- ----- ---- ------- 1 False do something great 2 Brian False repeat 3 Okken False again and again $ tasks list -o Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat $ tasks list --owner Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat 

Es sieht ziemlich einfach aus. Der Befehl tasks list zeigt eine Liste aller Aufgaben unter der Überschrift an.
Der Titel wird gedruckt, auch wenn die Liste leer ist. Der Befehl zeigt nur Daten von einem Eigentümer an, wenn -o oder --owner . Und wie überprüfen wir das? Es gibt viele Möglichkeiten, aber wir werden Moki verwenden.


Tests, die MOKs verwenden, sind notwendigerweise White-Box-Tests , und wir müssen den Code untersuchen, um zu entscheiden, was und wo wir schlagen werden. Der Haupteinstiegspunkt ist hier:


ch7 / task_proj_v2 / src / task / cli.py

 if __name__ == '__main__': tasks_cli() 

Dies ist nur ein Aufruf von tasks_cli() :


ch7 / task_proj_v2 / src / task / cli.py

 @click.group(context_settings={'help_option_names': ['-h', '--help']}) @click.version_option(version='0.1.1') def tasks_cli(): """Run the tasks application.""" pass 

Offensichtlich? Nein. Aber warte, es wird gut (oder schlecht, abhängig von deiner Sichtweise). Hier ist einer der list :


ch7 / task_proj_v2 / src / task / cli.py

 @tasks_cli.command(name="list", help="list tasks") @click.option('-o', '--owner', default=None, help='list tasks with this owner') def list_tasks(owner): """    .   ,      . """ formatstr = "{: >4} {: >10} {: >5} {}" print(formatstr.format('ID', 'owner', 'done', 'summary')) print(formatstr.format('--', '-----', '----', '-------')) with _tasks_db(): for t in tasks.list_tasks(owner): done = 'True' if t.done else 'False' owner = '' if t.owner is None else t.owner print(formatstr.format( t.id, owner, done, t.summary)) 

Wenn Sie sich an das Schreiben von Click-Code gewöhnt haben, stellen Sie sicher, dass dieser Code nicht so schlecht ist. Ich werde hier nicht erklären, was und wie es in dieser Funktion funktioniert, da die Entwicklung von Befehlszeilencode nicht im Mittelpunkt des Buches steht. Obwohl ich fast absolut sicher bin, dass ich diesen richtigen Code habe, gibt es immer viel Raum für menschliches Versagen. Aus diesem Grund ist eine gute Reihe automatisierter Tests wichtig, um sicherzustellen, dass diese Funktion ordnungsgemäß funktioniert.
Diese Funktion list_tasks(owner) hängt von mehreren anderen Funktionen ab: tasks_db() und tasks.list_tasks(owner) (API-Funktion).


Wir werden mock , um gefälschte Funktionen für tasks_db() und tasks.list_tasks() . Dann können wir die list_tasks Methode über die Befehlszeilenschnittstelle aufrufen und sicherstellen, dass sie die tasks.list_tasks() -Funktion tasks.list_tasks() , die korrekt funktioniert und den Rückgabewert korrekt verarbeitet.
tasks_db() wir uns eine echte Implementierung an, um tasks_db() zu übertönen:


Zurück

Source: https://habr.com/ru/post/de448798/


All Articles