[PYTHON] UpNext2 Development record # 2 Traffic information API-GET ~ Implémentation de la sauvegarde de fichiers et de pytest-mock

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.

1. Structure du projet

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.

image.png

2. Présentation du module principal

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)

Explication a1: Contre-mesures pour l'importation relative Python addictive

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.

Explication a2: Division de chaîne avec () qui évite élégamment la limite de 79 caractères par ligne

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.

Commentaire A3: les requêtes d'exception intelligente lancent

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.

Explication a4: avec open est basique

La méthode ouverte de lecture et d'écriture de fichiers est essentiellement écrite avec with et fermée implicitement.

Explication a5: Gestion collective des exceptions

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

Explication a6: Répéter en boucle dans la routine principale

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.

3. Présentation du module de test

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__])

Explication b1: Demandes simulées, objet de réponse

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.

Explication b2: Gestion intégrée des exceptions par side_effect

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.

Explication b3: Modifier le comportement fictif par paramètre

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.

Explication b4: Changement des paramètres de test par @ pytest.mark.parametrize

@ 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.

Explication b5: Correctif requests.get

Accrochez l'appel à requests.get et remplacez l'objet de valeur de retour requests.Response par un faux.

Explication b6: Patcher ouvert

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 '.

Explication b7: Gestion des exceptions dans pytest

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.

Explication b8: Post-traitement lorsqu'une exception se produit

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.

Explication b9: Contrôle normal des résultats

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.

4. Présentation de .vscode / setting.json

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.

5. Résultats des tests

À 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

Recommended Posts

UpNext2 Development record # 2 Traffic information API-GET ~ Implémentation de la sauvegarde de fichiers et de pytest-mock
Mémorandum de sauvegarde et modèle de chargement
UpNext2 Development Record # 0 Essayez de définir des objectifs de développement V2