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.
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.
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)
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.
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.
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 ().
Die offene Methode zum Lesen und Schreiben von Dateien wird grundsätzlich mit with geschrieben und implizit geschlossen.
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
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.
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__])
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.
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.
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.
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.
Binden Sie den Aufruf an request.get ein und ersetzen Sie das Rückgabewert request.Response-Objekt durch ein Mock.
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.
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.
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.
Ü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.
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.
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