Il s'agit d'une continuation du dossier de développement UpNext2. Cette fois, j'écrirai le code réel et le testerai dans l'environnement Python CI dans le code VS précédent. En particulier, il n'y a pas beaucoup d'exemples de description sur l'utilisation de mock_open dans pytest sur le net, donc j'ai eu du mal, donc j'espère que cela sera utile.
Cet article concerne les appels d'API moqueurs à l'aide de pytest et pytest-mock, l'écriture de fichiers moqueurs avec mock_open, couvrant plusieurs branches conditionnelles avec mark.parametrize, les tests de gestion des exceptions avec side_effect et même les importations relatives. Comprend des sujets tels que les contre-mesures. Le code en cours de test fait l'appel API pour le Tokyo Public Transportation Open Data Challenge.
L'environnement prérequis pour cet article est 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 est.
Le code que j'ai créé cette fois est un commit le 14 juin 2020 à https://github.com/toast-uz/UpNext2/tree/develop.
Créez odpt_dump.py et son test en fonction de l'environnement créé la dernière fois. odpt_dump.py utilise l'API de vidage du Tokyo Public Transportation Open Data Challenge pour télécharger certains fichiers d'informations sur le trafic et les enregistrer tels quels dans le dossier local_data / odpt_dump.
Voici les principales sources du module principal. Explication Je vais expliquer chaque partie du commentaire.
odpt_dump.py
import requests
try: #Commentaire A1
from . import config_secret
except ImportError:
import config_secret
query_string = ('https://api-tokyochallenge.odpt.org/api/v4/odpt:{}.json'
'?acl:consumerKey={}') #Commentaire 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() #Commentaire A3
with open(save_path.format(rdf_type), 'wb') as save_file:
save_file.write(response.content) #Commentaire A4
print('done.')
except Exception as e: #Commentaire A5
print('fail, due to: {}'.format(e))
raise
if __name__ == '__main__':
for rdf_type in [ #Commentaire A6
'Calendar',
'Operator',
'Station',
'StationTimetable',
'TrainTimetable',
'TrainType',
'RailDirection',
'Railway']:
get_and_save(rdf_type)
Dans config_secret.py, qui est le même dossier que odpt_dump.py, la clé API pour les données ouvertes des transports publics de Tokyo donnée aux développeurs individuels est définie. Je pense que cette structure de projet est relativement standard, mais le succès et l'échec différeront en fonction de la méthode d'importation entre l'exécution du programme et l'exécution du test.
Méthode d'exécution | .Avec import | .Aucune importation |
---|---|---|
Exécutez le programme sous forme de fichier | Échec*1 | Succès |
Exécution du programme en tant que module | Succès | Échec*2 |
Essai | Succès | Échec*2 |
*1: ImportError: attempted relative import with no known parent package *2: ModuleNotFoundError: No module named 'config_secret'
Si vous souhaitez que le programme soit exécuté en tant que module, spécifiez preprocess.src.odpt_dump.
En tant que route royale, je pense que vous devriez unifier pour importer avec et sélectionner l'exécution dans le module lors de l'exécution du programme. Cependant, comme il est plus facile d'exécuter le programme en tant que fichier, je l'ai implémenté de manière à ce que ce soit un peu délicat, mais plusieurs méthodes d'importation sont alignées et commutées lorsqu'une erreur est détectée.
En règle générale de pep8, il existe une règle de 79 caractères ou moins par ligne, et si cela n'est pas satisfait, une erreur se produira. À ce moment-là, la méthode de fractionnement d'une longue chaîne de caractères n'utilise pas d'échappement ou de concaténation avec +, et il est élégant d'utiliser this (). Veuillez noter que ce n'est pas un tapple.
En réponse aux demandes, il est judicieux de lancer toute erreur HTTP autre que HTTP200 comme exception. Il existe une fonction intégrée rise_for_status () pour cela.
La méthode ouverte de lecture et d'écriture de fichiers est essentiellement écrite avec with et fermée implicitement.
Non seulement les erreurs HTTP, mais toutes les exceptions en cours de route, y compris les fichiers, sont détectées ici. Il se déclenche immédiatement, donc peu importe si vous ne l'avez pas, mais vous avez l'impression de le traiter correctement. Lol
En général, le processus get_and_save ci-dessus est répété sur plusieurs fichiers cibles de téléchargement. Le format de liste d'entrée est le moyen le plus basique d'écrire une boucle Python.
En fait, au début, j'ai écrit solidement en main au lieu de le diviser en modules. Cependant, lorsqu'il s'agit de tests, il est important de minimiser la description de main et de la décomposer en classes et modules de taille raisonnable.
Voici le module de test. Explication Je vais expliquer chaque partie du commentaire.
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() #Commentaire b1
mock_resp.raise_for_status = mocker.Mock()
if not is_normal:
mock_resp.raise_for_status.side_effect = requests.exceptions.HTTPError(
http404_msg) #Commentaire b2
mock_resp.status_code = 200 if is_normal else 404 #Commentaire b3
mock_resp.content = b'TEST'
return mock_resp
@pytest.mark.parametrize('is_normal', [ #Commentaire 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 #Commentaire b5
mock_file = mocker.mock_open()
mocker.patch('builtins.open', mock_file) #Commentaire b6
with pytest.raises(Exception) as e: #Commentaire b7
odpt_dump.get_and_save('Dummy')
raise
if (not is_normal) and (str(e.value) is http404_msg): #Commentaire b8
return
assert mock_file.call_count == 1 #Commentaire b9
assert mock_file().write.call_args[0][0] == mock_resp.content
if __name__ == '__main__':
pytest.main(['-v', __file__])
Dans pytest, vous pouvez vous moquer de l'équivalent de MagicMock avec cette description pour les objets et les fonctions. Lorsque le test est exécuté, l'objet ou la fonction correspondant de la méthode cible est automatiquement remplacé par la maquette, et le processus passe à la maquette prédéfinie. C'est la première fois que j'utilise un simulacre, et je pensais que c'était un mécanisme diaboliquement mystérieux. Conceptuellement similaire à un hook d'API.
Lorsque vous vous moquez d'un objet, les propriétés peuvent simplement être pseudo-implémentées, mais la méthode doit être associée à une autre simulation en tant que fonction. Bien entendu, les propriétés et méthodes qui ne sont pas utilisées dans le code exécutable n'ont pas besoin d'être pseudo-implémentées. C'est juste une simulation, vous n'avez donc qu'à vous rendre là où vous pouvez le voir.
Dans ce cas, rise_for_status () est la cible fictive. De plus, utilisez side_effect si vous souhaitez lever une exception dans le processus plutôt que de simplement renvoyer le résultat du processus de la fonction.
Comme is_normal est un paramètre de test, il est facile de modifier les propriétés de l'objet fictif en fonction de sa valeur. Contrairement à cela, il semble que side_effect soit nécessaire lors du changement de comportement en fonction des paramètres d'entrée du simulacre lors de l'exécution du test. C'est déroutant, mais c'est une chose différente.
@ pytest.mark.parametrize vous permet de changer les paramètres de test et de répéter les tests. C'est plus intelligent que d'implémenter la commutation avec des boucles ou des ifs dans un test, et l'exécution des tests est perçue comme un autre test indépendant, ce qui facilite le travail avec VSCode.
Il est également possible de basculer en tant que jeux de paramètres multiples en décrivant les paramètres dans tapple.
Accrochez l'appel à requests.get et remplacez l'objet de valeur de retour requests.Response par un faux.
Remplacez la méthode ouverte par le mock spécial mock_open. Ici, il existe de nombreux exemples où la méthode open est exprimée par '\ _ \ _ main__. Open', mais cela ne fonctionne pas et il est nécessaire de l'exprimer comme'builtins.open '.
La spécification de pytest est que si une exception se produit pendant l'exécution du code, le test est arrêté et le test est considéré comme réussi. Il semble que s'arrêter à une exception soit le comportement correct pour le code. Par conséquent, il est nécessaire pour pytest de décrire explicitement la gestion des exceptions pour déterminer si l'exception a été levée comme prévu ou si une exception involontaire s'est produite.
Vous devez écrire une instruction with pytest.raises pour exécuter le code testé qui déclenche l'exception dans sa portée with.
Déterminez si l'exception s'est produite comme prévu, en dehors de avec. Ici, nous vérifions si les paramètres de test et l'exception de l'erreur 404 définie par mock se produisent comme prévu.
Vérifiez si l'écriture dans le fichier (remplacé par Mock) réussit dans le cas où aucune exception ne s'est produite. La caractéristique de pytest est que vous pouvez vérifier le résultat du test avec une simple instruction assert.
J'expliquerai principalement setting.json, qui décrit les paramètres de test. Option de ligne de commande lorsque pytestArgs démarre pytest à partir de VS Code.
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 doit être la série pytest v5? Supprimez les alertes qui apparaissent. --cov est un paramètre pour afficher la couverture. Vous pouvez également vérifier la couverture de branche conditionnelle avec --cov-branch et clarifier les pièces non testées par numéro de ligne avec --cov-report = term-missing.
Les options détaillées ici ne sont pas intégrées à l'interface utilisateur des paramètres VSCode, et si vous modifiez le paramètre pytest dans l'interface utilisateur des paramètres VSCode, vous devez modifier le fichier directement et décrire le paramètre. Il y a.
À l'exception de l'exception d'importation de odpt_dump.py et de la routine principale, tous les tests couverts ont été exécutés et ont réussi.
============================= 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) signifie omis au milieu