4 Techniken zum Testen von Python Command Line (CLI) -Apps

4 Techniken zum Testen von Python Command Line (CLI) -Apps

Sie haben gerade Ihre erste Python-Befehlszeilen-App erstellt. Oder vielleicht dein zweiter oder dritter. Sie haben Python schon eine Weile gelernt und sind jetzt bereit, som etwas Größeres und Komplexeres zu erstellen , kann aber weiterhin über eine Befehlszeile ausgeführt werden. Oder Sie sind es gewohnt, Webanwendungen oder Desktop-Apps mit einer grafischen Benutzeroberfläche zu building and testing, beginnen jedoch jetzt mit der Erstellung von CLI-Anwendungen.

In all diesen und weiteren Situationen müssen Sie die verschiedenen Methoden zum Testen einer Python CLI-Anwendung kennenlernen und sich damit vertraut machen.

Obwohl die Auswahl der Werkzeuge einschüchternd sein kann, sollten Sie vor allem berücksichtigen, dass Sie nur die von Ihrem Code generierten Ausgaben mit den erwarteten Ausgaben vergleichen. Daraus folgt alles.

*In diesem Tutorial lernen Sie vier praktische Techniken zum Testen von Python-Befehlszeilen-Apps kennen:*
  • "Lo-Fi" -Debugging mit "+ print () +"

  • Verwenden eines visuellen Python-Debuggers

  • Unit Testing mit Pytest und Mocks

  • Integrationstests

    *Kostenloser Bonus:* Link: # [Klicken Sie hier, um unser Python-Test-Spickzettel zu erhalten], in dem die in diesem Tutorial vorgestellten Techniken zusammengefasst sind.

Alles wird um eine grundlegende Python-CLI-App herum strukturiert, die Daten in Form eines mehrstufigen Wörterbuchs an zwei Funktionen übergibt, die sie auf irgendeine Weise transformieren, und sie dann an den Benutzer druckt.

Wir werden den folgenden Code verwenden, um einige der verschiedenen Methoden zu untersuchen, die Ihnen beim Testen helfen. Und obwohl dies sicherlich nicht erschöpfend ist, hoffe ich, dass dieses Tutorial Ihnen genug Breite bietet, damit Sie sicher sein können, effektive Tests in den wichtigsten Testbereichen zu erstellen.

Ich habe in diesem ersten Code einige Fehler eingestreut, die wir mit unseren Testmethoden aufdecken werden.

_ Hinweis : Der Einfachheit halber enthält dieser Code keine grundlegenden Best Practices, z. B. die Überprüfung der Existenz von Schlüsseln in einem Wörterbuch. _

Lassen Sie uns als ersten Schritt in jeder Phase dieser Anwendung über unsere Objekte nachdenken. Wir beginnen mit einer Struktur, die John Q beschreibt. Öffentlichkeit:

JOHN_DATA = {
    'name': 'John Q. Public',
    'street': '123 Main St.',
    'city': 'Anytown',
    'state': 'FL',
    'zip': 99999,
    'relationships': {
        'siblings': ['Michael R. Public', 'Suzy Q. Public'],
        'parents': ['John Q. Public Sr.', 'Mary S. Public'],
    }
}

Wir reduzieren dann die anderen Wörterbücher und erwarten dies, nachdem wir unsere erste Transformationsfunktion + initial_transform + aufgerufen haben:

JOHN_DATA = {
    'name': 'John Q. Public',
    'street': '123 Main St.',
    'city': 'Anytown',
    'state': 'FL',
    'zip': 99999,
    'siblings': ['Michael R. Public', 'Suzy Q. Public'],
    'parents': ['John Q. Public Sr.', 'Mary S. Public'],
}

Dann bauen wir alle Adressinformationen mit der Funktion + final_transform + in einen einzigen Adresseintrag:

JOHN_DATA = {
    'name': 'John Q. Public',
    'address': '123 Main St. \nAnytown, FL 99999'
    'siblings': ['Michael R. Public', 'Suzy Q. Public'],
    'parents': ['John Q. Public Sr.', 'Mary S. Public'],
}

Und der Aufruf von + print_person + schreibt dies an die Konsole:

Hello, my name is John Q. Public, my siblings are Michael R. Public
and Suzy Q. Public, my parents are John Q. Public Sr. and Mary S. Public,
and my mailing address is:
123 Main St.
Anytown, FL 99999

testapp.py:

def initial_transform(data):
    """
    Flatten nested dicts
    """
    for item in list(data):
        if type(item) is dict:
            for key in item:
                data[key] = item[key]

    return data


def final_transform(transformed_data):
    """
    Transform address structures into a single structure
    """
    transformed_data['address'] = str.format(
        "{0}\n{1}, {2} {3}", transformed_data['street'],
        transformed_data['state'], transformed_data['city'],
        transformed_data['zip'])

    return transformed_data


def print_person(person_data):
    parents = "and".join(person_data['parents'])
    siblings = "and".join(person_data['siblings'])
    person_string = str.format(
        "Hello, my name is {0}, my siblings are {1}, "
        "my parents are {2}, and my mailing"
        "address is: \n{3}", person_data['name'],
        parents, siblings, person_data['address'])
    print(person_string)


john_data = {
    'name': 'John Q. Public',
    'street': '123 Main St.',
    'city': 'Anytown',
    'state': 'FL',
    'zip': 99999,
    'relationships': {
        'siblings': ['Michael R. Public', 'Suzy Q. Public'],
        'parents': ['John Q. Public Sr.', 'Mary S. Public'],
    }
}

suzy_data = {
    'name': 'Suzy Q. Public',
    'street': '456 Broadway',
    'apt': '333',
    'city': 'Miami',
    'state': 'FL',
    'zip': 33333,
    'relationships': {
        'siblings': ['John Q. Public', 'Michael R. Public',
                    'Thomas Z. Public'],
        'parents': ['John Q. Public Sr.', 'Mary S. Public'],
    }
}

inputs = [john_data, suzy_data]

for input_structure in inputs:
    initial_transformed = initial_transform(input_structure)
    final_transformed = final_transform(initial_transformed)
    print_person(final_transformed)

Im Moment erfüllt der Code diese Erwartungen nicht wirklich, daher werden wir die vier Techniken untersuchen, während wir sie kennenlernen. Auf diese Weise erhalten Sie praktische Erfahrungen im Umgang mit diesen Techniken, erweitern Ihre Komfortzone und lernen, für welche Probleme sie am besten geeignet sind.

"Lo-Fi" -Debugging mit Print

Dies ist eine der einfachsten Möglichkeiten zum Testen. Alles, was Sie hier tun müssen, ist "+ print +" eine Variable oder ein Objekt, an dem Sie interessiert sind - vor einem Funktionsaufruf, nach einem Funktionsaufruf oder innerhalb einer Funktion.

Mit diesen können Sie den Eingang einer Funktion, den Ausgang einer Funktion und die Logik einer Funktion überprüfen.

Wenn Sie den obigen Code als "+ testapp.py " speichern und versuchen, ihn mit " python testapp.py +" auszuführen, wird ein Fehler wie der folgende angezeigt:

Traceback (most recent call last):
  File "testapp.py", line 60, in <module>
    print_person(final_transformed)
  File "testapp.py", line 23, in print_person
    parents = "and".join(person_data['parents'])
KeyError: 'parents'

In + person_data + fehlt ein Schlüssel, der an + print_person + übergeben wird. Der erste Schritt wäre, die Eingabe in "+ print_person " zu überprüfen und festzustellen, warum unsere erwartete Ausgabe (eine gedruckte Nachricht) nicht generiert wird. Wir werden nur einen Funktionsaufruf " print " vor dem Aufruf von " print_person +" hinzufügen:

final_transformed = final_transform(initial_transformed)
print(final_transformed)
print_person(final_transformed)

Die Funktion "+ print " erledigt hier die Aufgabe und zeigt in ihrer Ausgabe, dass wir weder die oberste Ebene " Eltern " noch die Taste " Geschwister " haben, aber im Interesse unserer Vernunft werde ich zeige dir ` pprint `, das mehrstufige Objekte besser lesbar druckt. Fügen Sie dazu " from pprint import pprint +" oben in Ihr Skript ein.

Anstelle von "+ print (final_transformed) " rufen wir " pprint (final_transformed) +" auf, um unser Objekt zu untersuchen:

{'address': '123 Main St.\nFL, Anytown 99999',
 'city': 'Anytown',
 'name': 'John Q. Public',
 'relationships': {'parents': ['John Q. Public Sr.', 'Mary S. Public'],
                   'siblings': ['Michael R. Public', 'Suzy Q. Public']},
 'state': 'FL',
 'street': '123 Main St.',
 'zip': 99999}

Vergleichen Sie dies mit der erwarteten endgültigen Form oben.

Da wir wissen, dass + final_transform + das Wörterbuch + Beziehungen + nicht berührt, ist es Zeit zu sehen, was in + initial_transform + vor sich geht. Normalerweise würde ich einen herkömmlichen Debugger verwenden, um dies zu durchlaufen, aber ich möchte Ihnen eine andere Verwendung des Druck-Debuggens zeigen.

Wir können den Status von Objekten im Code drucken, sind aber nicht darauf beschränkt. Wir können drucken, was wir wollen, also können wir auch Markierungen drucken, um zu sehen, welche Logikzweige wann ausgeführt werden.

Da es sich bei + initial_transform + in erster Linie um einige Schleifen handelt und interne Wörterbücher von der inneren for-Schleife verarbeitet werden sollen, sollten wir überprüfen, was dort passiert, wenn überhaupt:

def initial_transform(data):
    """
    Flatten nested dicts
    """
    for item in list(data):
        if type(item) is dict:
            print "item is dict!"
            pprint(item)
            for key in item:
                data[key] = item[key]

    return data

Wenn wir in unserer Eingabe "+ Daten +" auf ein Wörterbuch stoßen, werden wir in der Konsole benachrichtigt und sehen, wie das Element aussieht.

Nach dem Ausführen hat sich unsere Konsolenausgabe nicht geändert. Dies ist ein guter Beweis dafür, dass unsere "+ if +" - Anweisung nicht wie erwartet funktioniert. Während wir weiter drucken können, um den Fehler zu finden, ist dies eine großartige Möglichkeit, die Stärken der Verwendung eines Debuggers zu demonstrieren.

Als Übung empfehle ich jedoch, diesen Code nur mit Print-Debugging zu suchen. Dies ist eine gute Vorgehensweise und zwingt Sie dazu, über alle Möglichkeiten nachzudenken, wie Sie die Konsole verwenden können, um Sie über verschiedene Ereignisse im Code zu informieren.

Einpacken

*Wann wird das Druck-Debugging verwendet?*
  • Einfache Objekte

  • Kürzere Skripte

  • Scheinbar einfache Fehler

  • Schnelle Inspektionen

    *Tiefer tauchen:*
  • pprint - Druckobjekte verschönern

    *Vorteile:*
  • Schnelltests

  • Einfach zu verwenden

    *Nachteile:*
  • In den meisten Fällen müssen Sie das gesamte Programm ausführen, andernfalls:

  • Sie müssen zusätzlichen Code hinzufügen, um den Fluss manuell zu steuern

  • Sie können den Testcode versehentlich verlassen, wenn Sie fertig sind, insbesondere bei komplexem Code

Verwenden eines Debuggers

Debugger eignen sich hervorragend, wenn Sie den Code zeilenweise durchlaufen und den gesamten Anwendungsstatus überprüfen möchten. Sie helfen, wenn Sie ungefähr wissen, wo Fehler auftreten, aber nicht herausfinden können, warum, und sie geben Ihnen einen guten Überblick über alles, was in Ihrer Anwendung gleichzeitig passiert.

Es gibt viele Debugger, und oft kommen sie mit IDEs. Python hat auch a Modul namens + pdb +, das in REPL verwendet werden kann Debug-Code. Anstatt auf implementierungsspezifische Details aller verfügbaren Debugger einzugehen, werde ich Ihnen in diesem Abschnitt zeigen, wie Sie Debugger mit allgemeinen Funktionen verwenden, z. B. das Festlegen von Breakpoints und Watches.

*Haltepunkte* sind Markierungen in Ihrem Code, die Ihrem Debugger mitteilen, wo die Ausführung angehalten werden soll, damit Sie Ihren Anwendungsstatus überprüfen können. *Watches* sind Ausdrücke, die Sie während einer Debugging-Sitzung hinzufügen können, um den Wert einer Variablen (und mehr) zu überwachen. Sie werden während der Ausführung Ihrer App beibehalten.

Kehren wir jedoch zu den Haltepunkten zurück. Diese werden dort hinzugefügt, wo Sie eine Debugging-Sitzung starten oder fortsetzen möchten. Da wir die Methode + initial_transform + debuggen, möchten wir dort eine einfügen. Ich werde den Haltepunkt mit einem "+ (*) +" bezeichnen:

def initial_transform(data):
    """
    Flatten nested dicts
    """
(*) for item in list(data):
        if type(item) is dict:
            for key in item:
                data[key] = item[key]

    return data

Wenn wir jetzt mit dem Debuggen beginnen, wird die Ausführung in dieser Zeile angehalten und Sie können Variablen und ihre Typen an diesem bestimmten Punkt in der Programmausführung sehen. Wir haben einige Optionen, um in unserem Code zu navigieren: step over, step in und step out sind die häufigsten.

Schritt über wird am häufigsten verwendet - dies springt einfach zur nächsten Codezeile.

Step in, versucht tiefer in den Code einzudringen. Sie werden dies verwenden, wenn Sie auf einen Funktionsaufruf stoßen, den Sie genauer untersuchen möchten. Sie werden direkt zum Code dieser Funktion weitergeleitet und können dort den Status überprüfen. Sie verwenden es auch häufig, wenn Sie es für step over verwechseln. Glücklicherweise kann step out uns retten, dies bringt uns zurück zum Anrufer.

Wir können hier auch eine watch setzen, so etwas wie + type (item) is dict +, was Sie in den meisten IDEs über eine Schaltfläche 'watch hinzufügen' während einer Debugging-Sitzung tun können. Dies zeigt nun "+ True " oder " False +" an, egal wo Sie sich im Code befinden.

Stellen Sie die Uhr ein und gehen Sie jetzt über die Uhr, sodass Sie in der Zeile + angehalten werden, wenn Typ (Element) diktiert ist: +. Sie sollten nun in der Lage sein, den Status der Uhr, die neue Variable "+ item " und das Objekt " data +" zu sehen.

Python Debugger Screenshot: Variablen beobachten

Auch ohne die Uhr können wir das Problem erkennen: Anstatt "+ Typ " zu betrachten, auf was " Element " zeigt, wird der Typ " Element +" selbst betrachtet, bei dem es sich um eine Zeichenfolge handelt. Computer machen schließlich genau das, was wir ihnen sagen. Dank des Debuggers sehen wir den Fehler unserer Wege und korrigieren unseren Code wie folgt:

def initial_transform(data):
    """
    Flatten nested dicts
    """
    for item in list(data):
        if type(data[item]) is dict:
            for key in data[item]:
                data[key] = item[key]

    return data

Wir sollten es erneut durch den Debugger laufen lassen und sicherstellen, dass der Code dahin geht, wo wir ihn erwarten. Und das sind wir nicht, die Struktur sieht jetzt so aus:

john_data = {
    'name': 'John Q. Public',
    'street': '123 Main St.',
    'city': 'Anytown',
    'state': 'FL',
    'zip': 99999,
    'relationships': {
        'siblings': ['Michael R. Public', 'Suzy Q. Public'],
        'parents': ['John Q. Public Sr.', 'Mary S. Public'],
    },
    'siblings',
    'parents',
}

Nachdem wir uns nun die Verwendung eines visuellen Debuggers angesehen haben, gehen wir tiefer und stellen unser neues Wissen auf die Probe, indem wir die folgende Übung abschließen.

Wir haben über den visuellen Debugger gesprochen. Wir haben den visuellen Debugger verwendet. Wir lieben den visuellen Debugger. Diese Technik hat jedoch immer noch Vor- und Nachteile, die Sie im folgenden Abschnitt überprüfen können.

Einpacken

*Wann man einen Python-Debugger verwendet:*
  • Komplexere Projekte

  • Fehler sind schwer zu erkennen

  • Sie müssen mehr als ein Objekt untersuchen

  • Sie haben eine ungefähre Vorstellung davon, wo ein Fehler auftritt, müssen sich aber darauf konzentrieren

    *Tiefer tauchen:*
  • Bedingte Haltepunkte

  • Ausdrücke beim Debuggen auswerten

    *Vorteile:*
  • Kontrolle über den Programmfluss

  • Vogelperspektive des Anwendungsstatus

  • Sie müssen nicht genau wissen, wo der Fehler auftritt

    *Nachteile:*
  • Es ist schwierig, sehr große Objekte manuell zu betrachten

  • Das Debuggen von Code mit langer Laufzeit dauert sehr lange

Unit Testing mit Pytest und Mocks

Die vorherigen Techniken sind langwierig und können Codeänderungen erfordern, wenn Sie Eingabe-Ausgabe-Kombinationen ausführlich testen möchten, um sicherzustellen, dass Sie jeden Zweig Ihres Codes treffen - insbesondere, wenn Ihre App wächst. In unserem Beispiel sieht die Ausgabe von "+ initial_transform +" immer noch nicht ganz richtig aus.

Während die Logik in unserem Code ziemlich einfach ist, kann sie leicht an Größe und Komplexität zunehmen oder in die Verantwortung eines ganzen Teams fallen. Wie testen wir eine Anwendung strukturierter, detaillierter und automatisierter?

Geben Sie Unit-Tests ein.

Unit Testing ist eine Testtechnik, bei der der Quellcode in erkennbare Einheiten (normalerweise Methoden oder Funktionen) zerlegt und einzeln getestet wird.

Sie schreiben im Wesentlichen ein Skript oder eine Gruppe von Skripten, die jede Methode mit unterschiedlichen Eingaben testen, um sicherzustellen, dass jeder Logikzweig innerhalb jeder Methode getestet wird. Dies wird als Codeabdeckung bezeichnet, und normalerweise möchten Sie eine 100% ige Codeabdeckung anstreben. Dies ist nicht immer notwendig oder praktisch, aber wir können es für einen anderen Artikel (oder ein Lehrbuch) speichern.

Bei jedem Test wird die zu testende Methode isoliert behandelt: Externe Aufrufe werden mit einer Technik namens Mocking überschrieben, um zuverlässige Rückgabewerte zu erhalten, und alle Objekte, die vor dem Entfernen des Tests eingerichtet wurden, werden nach dem Test entfernt. Diese und andere Techniken werden durchgeführt, um die Unabhängigkeit und Isolation der zu testenden Einheit sicherzustellen.

Wiederholbarkeit und Isolation sind der Schlüssel für diese Art von Tests, obwohl wir weiterhin das Thema des Vergleichs der erwarteten Ergebnisse mit den tatsächlichen Ergebnissen fortsetzen. Nachdem Sie sich mit Unit-Tests insgesamt vertraut gemacht haben, können Sie einen kurzen Umweg machen und sehen, wie Sie Flask-Anwendungen mit der minimum brauchbaren Testsuite Unit-Tests durchführen .

Pytest

Nachdem wir uns nun wahrscheinlich etwas zu tief mit der Theorie befasst haben, schauen wir uns an, wie dies in der Praxis funktioniert. Python wird mit einem integrierten Modul "+ unittest " geliefert, aber ich glaube, dass " pytest " einen großartigen Job darin macht, auf dem aufzubauen, was " unittest +" bietet. In beiden Fällen zeige ich nur die Grundlagen des Komponententests, da der Komponententest allein mehrere lange Artikel umfassen kann.

Eine übliche Konvention besteht darin, alle Ihre Tests in einem "+ test " - Verzeichnis innerhalb Ihres Projekts abzulegen. Da es sich um ein kleines Skript handelt, ist eine Datei " test_testapp.py " auf derselben Ebene wie " testapp.py +" ausreichend.

Wir werden einen Komponententest für + initial_transform + schreiben, um zu zeigen, wie eine Reihe von erwarteten Ein- und Ausgängen eingerichtet und sichergestellt wird, dass sie übereinstimmen. Das Grundmuster, das ich mit + pytest + verwende, besteht darin, ein fixture einzurichten, das einige Parameter verwendet und diese zum Generieren der erwarteten und erwarteten Testeingaben verwendet Ausgänge, die ich will.

Zuerst zeige ich das Fixture-Setup und denke beim Nachdenken über den Code über die Testfälle nach, die Sie benötigen, um alle möglichen Zweige von + initial_transform + zu treffen:

import pytest
import testapp as app

@pytest.fixture(params=['nodict', 'dict'])
def generate_initial_transform_parameters(request):

Bevor wir Eingaben generieren, schauen wir uns an, was hier vor sich geht, da dies verwirrend werden kann.

Zuerst verwenden wir den Dekorator + @ pytest.fixture +, um die folgende Funktionsdefinition als Fixture zu deklarieren. Wir verwenden auch einen benannten Parameter + params +, um ihn mit` + generate_initial_transform_parameters + `zu verwenden.

Das Schöne daran ist, dass die dekorierte Funktion bei jeder Verwendung mit jedem Parameter verwendet wird. Wenn Sie also nur "+ generate_initial_transform_parameters " aufrufen, wird sie zweimal aufgerufen, einmal mit " nodict " als Parameter und einmal mit " dict +" `.

Um auf diese Parameter zuzugreifen, fügen wir unserer Funktionssignatur das pytest-Spezialobjekt + request + hinzu.

Lassen Sie uns nun unsere Inputs und erwarteten Outputs erstellen:

@pytest.fixture(params=['nodict', 'dict'])
def generate_initial_transform_parameters(request):
    test_input = {
        'name': 'John Q. Public',
        'street': '123 Main St.',
        'city': 'Anytown',
        'state': 'FL',
        'zip': 99999,
    }
    expected_output = {
        'name': 'John Q. Public',
        'street': '123 Main St.',
        'city': 'Anytown',
        'state': 'FL',
        'zip': 99999,
    }

    if request.param == 'dict':
        test_input['relastionships'] = {
            'siblings': ['Michael R. Public', 'Suzy Q. Public'],
            'parents': ['John Q. Public Sr.', 'Mary S. Public'],
        }
        expected_output['siblings'] = ['Michael R. Public', 'Suzy Q. Public']
        expected_output['parents'] = ['John Q. Public Sr.', 'Mary S. Public']

    return test_input, expected_output

Hier ist es nicht allzu überraschend, dass wir die Eingabe und die erwartete Ausgabe einrichten. Wenn wir den Parameter "" dict "" haben, ändern wir die Eingabe und die erwartete Ausgabe, sodass wir den Block "+ if +" testen können.

Dann schreiben wir den Test. Im Test müssen wir das Gerät als Parameter an die Testfunktion übergeben, um darauf zugreifen zu können:

def test_initial_transform(generate_initial_transform_parameters):
    test_input = generate_initial_transform_parameters[0]
    expected_output = generate_initial_transform_parameters[1]
    assert app.initial_transform(test_input) == expected_output

Testfunktionen sollten mit + test_ + vorangestellt werden und auf https://dbader.org/blog/python-assert-tutorial [+ assert + Anweisungen] basieren. Hier behaupten wir, dass die Ausgabe, die wir erhalten, wenn wir unsere Eingabe an unsere reale Funktion übergeben, unserer erwarteten Ausgabe entspricht. Wenn Sie dies entweder in Ihrer IDE mit einer Testkonfiguration oder mit "+ pytest +" in der CLI ausführen, werden …​ Fehler angezeigt! Unsere Ausgabe ist noch nicht ganz richtig. Beheben wir das Problem mithilfe der folgenden Übung: Die praktische Erfahrung ist von unschätzbarem Wert. Wenn Sie das Gelesene in die Praxis umsetzen, können Sie sich in Zukunft leichter daran erinnern.

Verspottet

Mocks sind ein weiterer wichtiger Bestandteil von Unit-Tests. Da wir nur eine einzelne Codeeinheit testen, ist es uns egal, was andere Funktionsaufrufe tun. Wir wollen nur eine verlässliche Rendite von ihnen.

Fügen wir + initial_transform + einen externen Funktionsaufruf hinzu:

def initial_transform(data):
    """
    Flatten nested dicts
    """
    for item in list(data):
        if type(data[item]) is dict:
            for key in data[item]:
                data[key] = data[item][key]
            data.pop(item)

    outside_module.do_something()
    return data

Wir möchten + do_something () + nicht live anrufen, sondern machen stattdessen einen Mock in unserem Testskript. Der Mock fängt diesen Anruf ab und gibt alles zurück, was Sie für den Mock eingestellt haben. Ich mag es, die Mocks in Fixtures einzurichten, da dies Teil des Test-Setups ist und wir den gesamten Setup-Code zusammenhalten können:

@pytest.fixture(params=['nodict', 'dict'])
def generate_initial_transform_parameters(request, mocker):
    [...]
    mocker.patch.object(outside_module, 'do_something')
    mocker.do_something.return_value(1)
    [...]

Jedes Mal, wenn Sie "+ initial_transform " aufrufen, wird der Aufruf " do_something +" abgefangen und gibt 1 zurück. Sie können auch Fixture-Parameter nutzen, um zu bestimmen, was Ihr Mock zurückgibt. Dies ist wichtig, wenn ein Code-Zweig durch das Ergebnis des externen Aufrufs bestimmt wird.

Ein letzter netter Trick ist die Verwendung von "+ side_effect +". Auf diese Weise können Sie unter anderem verschiedene Rückgaben für aufeinanderfolgende Aufrufe derselben Funktion verspotten:

def initial_transform(data):
    """
    Flatten nested dicts
    """
    for item in list(data):
        if type(data[item]) is dict:
            for key in data[item]:
                data[key] = data[item][key]
            data.pop(item)

    outside_module.do_something()
    outside_module.do_something()
    return data

Wir haben unser Modell so eingerichtet, dass eine Liste der Ausgaben (für jeden aufeinanderfolgenden Aufruf) an + side_effect + übergeben wurde:

@pytest.fixture(params=['nodict', 'dict'])
def generate_initial_transform_parameters(request, mocker):
    [...]
    mocker.patch.object(outside_module, 'do_something')
    mocker.do_something.side_effect([1, 2])
    [...]

Mocking ist sehr leistungsfähig, so leistungsfähig, dass Sie sogar Mock-Server einrichten können, um APIs von Drittanbietern zu testen, und ich ermutige Sie erneut alleine tiefer in das Verspotten mit + mocker + eintauchen.

Einpacken

*Wann werden Python-Unit-Test-Frameworks verwendet?*
  • Große, komplexe Projekte

  • OSS-Projekte

    *Hilfreiche Tools:*
  • Pytest fixtures

  • deepdiff zum Vergleichen komplexer Objekte

  • Spötter

    *Vorteile:*
  • Automatisiert laufende Tests

  • Kann viele Arten von Fehlern fangen

  • Einfache Einrichtung und Änderung für Teams

    *Nachteile:*
  • Mühsam zu schreiben

  • Muss mit den meisten Codeänderungen aktualisiert werden

  • Repliziert keine echte Anwendung, die ausgeführt wird

Integrationstests

Integrationstests sind hier eine der einfacheren Testmethoden, aber wohl eine der wichtigsten. Dies beinhaltet, dass Sie Ihre App tatsächlich durchgehend mit realen Daten in einer produktionsähnlichen Umgebung ausführen.

Unabhängig davon, ob es sich um Ihren Heimcomputer, einen Testserver, der einen Produktionsserver dupliziert, oder nur um das Ändern einer Verbindung zu einer Testdatenbank von einem Produktionsserver handelt, wissen Sie, dass Ihre Änderungen bei der Bereitstellung funktionieren.

Wie bei allen anderen Methoden überprüfen Sie, ob Ihre Anwendung bei einigen Eingaben die erwarteten Ausgaben generiert - außer dass Sie diesmal tatsächliche externe Module verwenden (im Gegensatz zu Unit-Tests, bei denen sie verspottet werden) und möglicherweise in tatsächliche Datenbanken schreiben oder Dateien und in größeren Anwendungen sicherstellen, dass Ihr Code gut in das Gesamtsystem integriert ist.

Wie Sie dies tun, hängt stark von Ihrer Anwendung ab. Beispielsweise kann unsere Test-App mit + python testapp.py + eigenständig ausgeführt werden. Stellen wir uns jedoch vor, unser Code sei ein Segment einer großen verteilten Anwendung wie einer ETL-Pipeline. In diesem Fall müssten Sie das gesamte System auf Testservern mit eingetauschtem Code ausführen, Daten durchlaufen und sicherstellen, dass er erstellt wurde es durch das ganze System in der richtigen Form. Außerhalb der Befehlszeilenanwendungswelt können Tools wie https://realpython.com/integration-testing-with-pyvows-and-django/[pyVows zum Integrationstest von Django-Apps verwendet werden.

Einpacken

*Wann werden Integrationstests in Python verwendet:*
  • Immer ;-)

  • Im Allgemeinen nach anderen Testmethoden, wenn sie angewendet werden.

    *Hilfreiche Tools:*
  • tox Umgebungs- und Testautomatisierungsmanagement

    *Vorteile:*
  • Sehen Sie, wie Ihre Anwendung unter realen Bedingungen ausgeführt wird

    *Nachteile:*
  • Bei größeren Anwendungen kann es schwierig sein, den Datenfluss genau zu verfolgen

  • Es müssen Testumgebungen vorhanden sein, die den Produktionsumgebungen sehr nahe kommen

Alles zusammenfügen

Zusammenfassend lässt sich sagen, dass bei allen CLI-Tests Ihre erwarteten Ergebnisse mit Ihren tatsächlichen Ergebnissen verglichen werden müssen, wenn bestimmte Eingaben vorliegen. Die Methoden, die ich oben besprochen habe, sind alle Möglichkeiten, genau das zu tun, und ergänzen sich in vielerlei Hinsicht. Dies sind wichtige Werkzeuge, die Sie verstehen sollten, wenn Sie weitere Befehlszeilenanwendungen in Python erstellen. Dieses Tutorial ist jedoch nur ein Ausgangspunkt.

Python verfügt über ein sehr reichhaltiges Ökosystem, das sich auch auf das Testen von Tools und Methoden erstreckt. Verzweigen Sie sich also davon und untersuchen Sie mehr - möglicherweise finden Sie ein Tool oder eine Technik, die ich hier nicht erwähnt habe und die Sie absolut lieben. Wenn ja, würde ich gerne in den Kommentaren davon hören!

Als kurze Zusammenfassung sind hier die Techniken, die wir heute gelernt haben und wie sie eingesetzt werden:

  • Debugging drucken - Drucken Sie Variablen und andere Markierungen im Code aus, um zu sehen, wie die Ausführung abläuft

  • Debugger - Steuern der Programmausführung, um den Anwendungsstatus und den Programmablauf aus der Vogelperspektive zu betrachten

  • Komponententest - Aufteilen einer Anwendung in einzeln testbare Einheiten und Testen aller Logikzweige innerhalb dieser Einheit

  • Integrationstest - Testen Ihrer Codeänderungen im Kontext der breiteren Anwendung

Jetzt geh und teste! Wenn Sie mit diesen Techniken arbeiten, teilen Sie mir in den Kommentaren unbedingt mit, wie Sie sie eingesetzt haben und welche Ihre Favoriten sind.

Klicken Sie auf den folgenden Link, um einen Python-Test-Spickzettel zu erhalten, in dem die in diesem Lernprogramm vorgestellten Techniken zusammengefasst sind:

*Kostenloser Bonus:* Link: # [Klicken Sie hier, um unser Python-Test-Spickzettel zu erhalten], in dem die in diesem Tutorial vorgestellten Techniken zusammengefasst sind.