[PYTHON] UpNext2 Entwicklungsdatensatz Nr. 2 Verkehrsinformationen API-GET ~ Implementierung von Dateispeicherung und Pytest-Mock

Dies ist eine Fortsetzung des UpNext2-Entwicklungsdatensatzes. Dieses Mal werde ich den eigentlichen Code schreiben und in der Python CI-Umgebung im vorherigen VS-Code testen. Insbesondere gibt es nicht viele Beispiele für die Verwendung von mock_open in pytest im Internet. Ich hatte es also schwer und hoffe, dass Sie es hilfreich finden.

In diesem Artikel geht es darum, API-Aufrufe mit pytest und pytest-mock zu verspotten, das Schreiben von Dateien mit mock_open zu verspotten, mehrere bedingte Zweige mit mark.parametrize abzudecken, Ausnahmebehandlungstests mit side_effect und sogar relative Importe. Enthält Themen wie Gegenmaßnahmen. Der zu testende Code führt den eigentlichen API-Aufruf für die Open Data Challenge von Tokyo Public Transport durch.

Die Voraussetzung für diesen Artikel ist Python 3.8.3 Pytest 5.4.2 ; plugins: cov-2.9.0, mock-3.1.1 VSCode 1.46.0 ; Python extention v2020.5.86806 ist.

Der Code, den ich dieses Mal erstellt habe, ist ein Commit am 14. Juni 2020 unter https://github.com/toast-uz/UpNext2/tree/develop.

1. Projektstruktur

Erstellen Sie odpt_dump.py und seinen Test basierend auf der zuletzt erstellten Umgebung. odpt_dump.py verwendet die Speicherauszugs-API der Open Data Challenge von Tokyo Public Transportation, um einige Verkehrsinformationsdateien herunterzuladen und im lokalen Ordner local_data / odpt_dump zu speichern.

image.png

2. Übersicht über das Hauptmodul

Nachfolgend finden Sie die Hauptquellen des Hauptmoduls. Erklärung Ich werde jeden Teil des Kommentars erklären.

odpt_dump.py


import requests

try:  #Kommentar a1
    from . import config_secret
except ImportError:
    import config_secret

query_string = ('https://api-tokyochallenge.odpt.org/api/v4/odpt:{}.json'
                '?acl:consumerKey={}')   #Kommentar a2
save_path = 'local_data/odpt_dump/{}.json'


def get_and_save(rdf_type):
    url = query_string.format(rdf_type, config_secret.apikey)
    print('Getting {}...'.format(rdf_type), end='', flush=True)
    try:
        response = requests.get(url)
        response.raise_for_status()  #Kommentar a3
        with open(save_path.format(rdf_type), 'wb') as save_file:
            save_file.write(response.content)  #Kommentar a4
        print('done.')
    except Exception as e:  #Kommentar a5
        print('fail, due to: {}'.format(e))
        raise


if __name__ == '__main__':
    for rdf_type in [  #Kommentar a6
            'Calendar',
            'Operator',
            'Station',
            'StationTimetable',
            'TrainTimetable',
            'TrainType',
            'RailDirection',
            'Railway']:
        get_and_save(rdf_type)

Erläuterung a1: Gegenmaßnahmen für den süchtig machenden relativen Python-Import

In config_secret.py, dem gleichen Ordner wie odpt_dump.py, wird der API-Schlüssel für Open Data von Tokyo Public Transportation definiert, der einzelnen Entwicklern übergeben wird. Ich denke, diese Projektstruktur ist relativ Standard, aber Erfolg und Misserfolg unterscheiden sich je nach Importmethode zwischen Programmausführung und Testausführung.

Ausführungsmethode .Mit Import .Kein Import
Führen Sie das Programm als Datei aus Fehler*1 Erfolg
Programmausführung als Modul Erfolg Fehler*2
Testlauf Erfolg Fehler*2

*1: ImportError: attempted relative import with no known parent package *2: ModuleNotFoundError: No module named 'config_secret'

Wenn das Programm als Modul ausgeführt werden soll, geben Sie preprocess.src.odpt_dump an.

Als königliche Straße denke ich, dass Sie sich zum Importieren vereinheitlichen sollten. Und wählen Sie die Ausführung im Modul aus, wenn Sie das Programm ausführen. Da es jedoch einfacher ist, das Programm als Datei auszuführen, habe ich es so implementiert, dass es etwas schwierig ist, aber mehrere Importmethoden aufgereiht und umgeschaltet werden, wenn ein Fehler auftritt.

Erläuterung a2: Aufteilen von Zeichenfolgen mit (), wodurch die Beschränkung auf 79 Zeichen pro Zeile auf elegante Weise vermieden wird

In der Regel von pep8 gibt es eine Regel von 79 Zeichen oder weniger pro Zeile. Wenn dies nicht erfüllt ist, tritt ein Fehler auf. Zu diesem Zeitpunkt verwendet die Methode zum Teilen einer langen Zeichenfolge kein Escape oder keine Verkettung mit +, und es ist elegant, this () zu verwenden. Bitte beachten Sie, dass es sich nicht um einen Tapple handelt.

Kommentar a3: Smart Exception wirft Anfragen ein

Als Antwort auf Anforderungen ist es sinnvoll, ausnahmsweise andere HTTP-Fehler als HTTP200 auszulösen. Dafür gibt es eine eingebaute Funktion raise_for_status ().

Erklärung a4: mit open ist grundlegend

Die offene Methode zum Lesen und Schreiben von Dateien wird grundsätzlich mit with geschrieben und implizit geschlossen.

Erläuterung a5: Ausnahmebehandlung gemeinsam

Hier werden nicht nur HTTP-Fehler, sondern alle Ausnahmen auf dem Weg, einschließlich Dateien, abgefangen. Es wird sofort ausgelöst, es spielt also keine Rolle, ob Sie es nicht haben, aber es fühlt sich an, als würden Sie es richtig verarbeiten. Lol

Erläuterung a6: Wiederholen Sie dies in einer Schleife in der Hauptroutine

Im Wesentlichen wird der obige Vorgang get_and_save für mehrere Download-Zieldateien wiederholt. Das For-In-Listenformat ist die einfachste Methode zum Schreiben einer Python-Schleife.

Eigentlich habe ich zuerst solide geschrieben, anstatt es in Module zu unterteilen. Beim Testen ist es jedoch wichtig, die Beschreibung von main zu minimieren und in Klassen und Module mit angemessener Größe zu unterteilen.

3. Übersicht über das Testmodul

Unten ist das Testmodul. Erklärung Ich werde jeden Teil des Kommentars erklären.

test_odpt_dump.py


from preprocess.src import odpt_dump
import pytest
import requests

http404_msg = '404 Not Found'


def _mock_response(mocker, is_normal):
    mock_resp = mocker.Mock()  #Kommentar b1
    mock_resp.raise_for_status = mocker.Mock()
    if not is_normal:
        mock_resp.raise_for_status.side_effect = requests.exceptions.HTTPError(
            http404_msg)  #Kommentar b2
    mock_resp.status_code = 200 if is_normal else 404  #Kommentar b3
    mock_resp.content = b'TEST'
    return mock_resp


@pytest.mark.parametrize('is_normal', [   #Kommentar b4
    True,
    False,
])
def test_get_and_save(mocker, is_normal):
    mock_resp = _mock_response(mocker, is_normal)
    mocker.patch('requests.get').return_value = mock_resp  #Kommentar b5

    mock_file = mocker.mock_open()
    mocker.patch('builtins.open', mock_file)  #Kommentar b6

    with pytest.raises(Exception) as e:  #Kommentar b7
        odpt_dump.get_and_save('Dummy')
        raise

    if (not is_normal) and (str(e.value) is http404_msg):  #Kommentar b8
        return

    assert mock_file.call_count == 1   #Kommentar b9
    assert mock_file().write.call_args[0][0] == mock_resp.content


if __name__ == '__main__':
    pytest.main(['-v', __file__])

Erläuterung b1: Verspotten von Anforderungen. Antwortobjekt

In pytest können Sie das Äquivalent von MagicMock mit dieser Beschreibung sowohl für Objekte als auch für Funktionen verspotten. Wenn der Test ausgeführt wird, wird das entsprechende Objekt oder die entsprechende Funktion der Zielmethode automatisch durch das Modell ersetzt, und der Prozess wechselt zum vordefinierten Modell. Dies ist das erste Mal, dass ich einen Schein benutze, und ich dachte, es sei ein teuflisch mysteriöser Mechanismus. Konzeptionell ähnlich einem API-Hook.

Erläuterung b2: Integrierte Ausnahmebehandlung durch side_effect

Wenn Sie ein Objekt verspotten, können die Eigenschaften einfach pseudoimplementiert werden, aber die Methode muss als Funktion einem weiteren Verspotten zugeordnet werden. Natürlich müssen Eigenschaften und Methoden, die nicht im ausführbaren Code verwendet werden, nicht pseudoimplementiert werden. Es ist nur ein Schein, also musst du es nur dort machen, wo du es sehen kannst.

In diesem Fall ist raise_for_status () das Scheinziel. Verwenden Sie außerdem side_effect, wenn Sie eine Ausnahme im Prozess auslösen möchten, anstatt nur das Ergebnis des Funktionsprozesses zurückzugeben.

Erläuterung b3: Ändern Sie das Scheinverhalten nach Parametern

Da is_normal ein Testparameter ist, ist es einfach, die Eigenschaften des Scheinobjekts basierend auf seinem Wert zu ändern. Im Gegensatz dazu scheint der Nebeneffekt erforderlich zu sein, wenn das Verhalten basierend auf den Eingabeparametern des Modells beim Ausführen des Tests geändert wird. Es ist verwirrend, aber es ist eine andere Sache.

Erläuterung b4: Umschalten der Testparameter durch @ pytest.mark.parametrize

Mit @ pytest.mark.parametrize können Sie Testparameter wechseln und Testläufe wiederholen. Es ist intelligenter als das Implementieren von Switching mit Schleifen oder Ifs innerhalb eines Tests. Die Testausführung wird als ein weiterer unabhängiger Test angesehen, der die Arbeit in VSCode erleichtert.

Es ist auch möglich, durch Beschreibung der Parameter in tapple als mehrere Parametersätze zu wechseln.

Erläuterung b5: Patchen von request.get

Binden Sie den Aufruf an request.get ein und ersetzen Sie das Rückgabewert request.Response-Objekt durch ein Mock.

Erläuterung b6: Patchen geöffnet

Ersetzen Sie die open-Methode durch das spezielle mock mock_open. Hier gibt es viele Beispiele für das Ausdrücken der open-Methode als '\ _ \ _ main__. Open', aber dies funktioniert nicht und Sie müssen sie als'builtins.open 'ausdrücken.

Erläuterung b7: Ausnahmebehandlung im Pytest

Die Spezifikation von pytest lautet: Wenn während der Codeausführung eine Ausnahme auftritt, wird der Test gestoppt und der Test als erfolgreich angesehen. Es scheint, dass das Stoppen bei einer Ausnahme das richtige Verhalten für den Code ist. Daher muss pytest die Ausnahmebehandlung explizit beschreiben, um festzustellen, ob die Ausnahme wie beabsichtigt ausgelöst wurde oder ob eine unbeabsichtigte Ausnahme aufgetreten ist.

Sie müssen eine with pytest.raises-Anweisung schreiben, um den zu testenden Code auszuführen, der die Ausnahme innerhalb seines with-Bereichs auslöst.

Erläuterung b8: Nachbearbeitung, wenn eine Ausnahme auftritt

Stellen Sie fest, ob die Ausnahme wie beabsichtigt außerhalb von mit aufgetreten ist. Hier prüfen wir, ob die Testparameter und die Ausnahme des von mock gesetzten 404-Fehlers wie beabsichtigt auftreten.

Erläuterung b9: Normale Ergebnisprüfung

Überprüfen Sie, ob das Schreiben in die Datei (ersetzt durch Mock) erfolgreich ist, wenn keine Ausnahme aufgetreten ist. Das Merkmal von pytest ist, dass Sie das Testergebnis mit einer einfachen assert-Anweisung überprüfen können.

4. Übersicht über .vscode / settings.json

Ich werde hauptsächlich settings.json erklären, das die Testeinstellungen beschreibt. Befehlszeilenoption, wenn pytestArgs pytest über VS Code startet.

setting.json


{
    "python.pythonPath": ".pyvenv/bin/python",
    "python.testing.pytestArgs": [
        "-o",
        "junit_family=xunit1",
        "--cov=preprocess/src",
        "--cov-branch",
        "--cov-report=term-missing",
        "preprocess",
    ],
    "python.testing.unittestEnabled": false,
    "python.testing.nosetestsEnabled": false,
    "python.testing.pytestEnabled": true,
    "python.linting.flake8Enabled": true,
    "python.linting.enabled": true
}

-o junit_family = xunit1 muss pytest v5 series sein? Unterdrücken Sie die angezeigten Warnungen. --cov ist eine Einstellung zum Anzeigen der Abdeckung. Sie können die bedingte Verzweigungsabdeckung auch mit --cov-branch überprüfen und nicht getestete Teile anhand der Zeilennummer mit --cov-report = term-missing klären.

Die detaillierten Optionen hier sind nicht in die Benutzeroberfläche der VSCode-Einstellungen integriert. Wenn Sie die Pytest-Einstellung in der Benutzeroberfläche der VSCode-Einstellungen ändern, müssen Sie die Datei direkt bearbeiten und die Einstellung beschreiben. Es gibt.

5. Testergebnisse

Mit Ausnahme der Importausnahme von odpt_dump.py und der Hauptroutine wurden alle abgedeckten Tests ausgeführt und waren erfolgreich.

============================= test session starts ==============================
(snip)
collected 2 items

preprocess/tests/test_odpt_dump.py ..                                    [100%]

(snip)
---------- coverage: platform darwin, python 3.8.3-final-0 -----------
Name                              Stmts   Miss Branch BrPart  Cover   Missing
-----------------------------------------------------------------------------
preprocess/src/__init__.py            0      0      0      0   100%
preprocess/src/config_secret.py       1      0      0      0   100%
preprocess/src/odpt_dump.py          22      4      4      1    73%   13-14, 35->36, 36-45
-----------------------------------------------------------------------------
TOTAL                                23      4      4      1    74%

============================== 2 passed in 0.46s ===============================

(snip) bedeutet in der Mitte weggelassen

Recommended Posts

UpNext2 Entwicklungsdatensatz Nr. 2 Verkehrsinformationen API-GET ~ Implementierung von Dateispeicherung und Pytest-Mock
Memorandum zum Speichern und Laden des Modells
UpNext2 Development Record # 0 Versuchen Sie, V2-Entwicklungsziele festzulegen