web-dev-qa-db-fra.com

Pytest où stocker les données attendues

Fonction de test J'ai besoin de passer des paramètres et de voir la sortie correspond à la sortie attendue.

C'est facile lorsque la réponse de la fonction est juste un petit tableau ou une chaîne d'une ligne qui peut être définie à l'intérieur de la fonction de test, mais supposons que la fonction que je teste modifie un fichier de configuration qui peut être énorme. Ou le tableau résultant est quelque chose de 4 lignes si je le définis explicitement. Où dois-je le stocker pour que mes tests restent propres et faciles à entretenir?

En ce moment, si c'est une chaîne, je mets juste un fichier près du test .py Et je le fais open() à l'intérieur du test:

def test_if_it_works():
    with open('expected_asnwer_from_some_function.txt') as res_file:
        expected_data = res_file.read()
    input_data = ... # Maybe loaded from a file as well
    assert expected_data == if_it_works(input_data)

Je vois de nombreux problèmes avec une telle approche, comme le problème de la mise à jour de ce fichier. Cela a aussi l'air mauvais. Je peux probablement améliorer les choses en déplaçant cela vers un appareil:

@pytest.fixture
def expected_data()
    with open('expected_asnwer_from_some_function.txt') as res_file:
        expected_data = res_file.read()
    return expected_data

@pytest.fixture
def input_data()
    return '1,2,3,4'

def test_if_it_works(input_data, expected_data):
    assert expected_data == if_it_works(input_data)

Cela déplace simplement le problème à un autre endroit et généralement je dois tester si la fonction fonctionne en cas d'entrée vide, d'entrée avec un seul élément ou plusieurs éléments, donc je devrais créer un gros appareil comprenant les trois cas ou plusieurs appareils. En fin de compte, le code devient assez désordonné.

Si une fonction attend un dictionnaire compliqué comme entrée ou rend le dictionnaire du même code de test de taille énorme devient laid:

 @pytest.fixture
 def input_data():
     # It's just an example
     return {['one_value': 3, 'one_value': 3, 'one_value': 3,
     'anotherky': 3, 'somedata': 'somestring'], 
      ['login': 3, 'ip_address': 32, 'value': 53, 
      'one_value': 3], ['one_vae': 3, 'password': 13, 'lue': 3]}

Il est assez difficile de lire les tests avec de tels appareils et de les tenir à jour.

Mettre à jour

Après avoir cherché un moment, j'ai trouvé une bibliothèque qui a résolu une partie d'un problème lorsque, au lieu de gros fichiers de configuration, j'avais de grandes réponses HTML. C'est betamax .

Pour une utilisation plus facile, j'ai créé un luminaire:

from betamax import Betamax

@pytest.fixture
def session(request):
    session = requests.Session()
    recorder = Betamax(session)
    recorder.use_cassette(os.path.join(os.path.dirname(__file__), 'fixtures', request.function.__name__)
    recorder.start()
    request.addfinalizer(recorder.stop)
    return session

Alors maintenant, dans mes tests, j'utilise simplement le dispositif session et chaque demande que je fais est sérialisée automatiquement dans le fichier fixtures/test_name.json Donc la prochaine fois que j'exécute le test au lieu de faire une vraie bibliothèque de requêtes HTTP le charge depuis le système de fichiers:

def test_if_response_is_ok(session):
   r = session.get("http://google.com")

C'est assez pratique car pour garder ces appareils à jour, j'ai juste besoin de nettoyer le dossier fixtures et de relancer mes tests.

27
Glueon

J'ai eu un problème similaire une fois, où je dois tester le fichier de configuration par rapport à un fichier attendu. Voilà comment je l'ai corrigé:

  1. Créez un dossier avec le même nom de votre module de test et au même emplacement. Mettez tous vos fichiers attendus dans ce dossier.

    test_foo/
        expected_config_1.ini
        expected_config_2.ini
    test_foo.py
    
  2. Créez un appareil chargé de déplacer le contenu de ce dossier vers un fichier temporaire. J'ai utilisé le luminaire tmpdir pour cette question.

    from __future__ import unicode_literals
    from distutils import dir_util
    from pytest import fixture
    import os
    
    
    @fixture
    def datadir(tmpdir, request):
        '''
        Fixture responsible for searching a folder with the same name of test
        module and, if available, moving all contents to a temporary directory so
        tests can use them freely.
        '''
        filename = request.module.__file__
        test_dir, _ = os.path.splitext(filename)
    
        if os.path.isdir(test_dir):
            dir_util.copy_tree(test_dir, bytes(tmpdir))
    
        return tmpdir
    
  3. Utilisez votre nouveau luminaire.

    def test_foo(datadir):
        expected_config_1 = datadir.join('expected_config_1.ini')
        expected_config_2 = datadir.join('expected_config_2.ini')
    

N'oubliez pas: datadir est identique à tmpdir fixture, plus la possibilité de travailler avec vos fichiers attendus placés dans le dossier a avec le nom même du module de test.

31
Fabio Menegazzo

Si vous n'avez que quelques tests, alors pourquoi ne pas inclure les données sous forme de chaîne littérale:

expected_data = """
Your data here...
"""

Si vous en avez une poignée, ou que les données attendues sont vraiment longues, je pense que votre utilisation d'appareils est logique.

Cependant, si vous en avez plusieurs, une solution différente serait peut-être préférable. En fait, pour un projet, j'ai plus d'une centaine de fichiers d'entrée et de sortie attendue. J'ai donc construit mon propre framework de test (plus ou moins). J'ai utilisé Nose, mais PyTest fonctionnerait également. J'ai créé un générateur de test qui parcourait le répertoire des fichiers de test. Pour chaque fichier d'entrée, un test a été produit qui a comparé la sortie réelle avec la sortie attendue (PyTest l'appelle paramétrage ). J'ai ensuite documenté mon framework pour que d'autres puissent l'utiliser. Pour revoir et/ou modifier les tests, vous ne modifiez que les fichiers d'entrée et/ou de sortie attendus et vous n'avez jamais besoin de regarder le fichier de test python. Pour permettre à différents fichiers d'entrée d'avoir différentes options défini, j'ai également créé un fichier de configuration YAML pour chaque répertoire (JSON fonctionnerait également pour réduire les dépendances). Les données YAML se composent d'un dictionnaire où chaque clé est le nom du fichier d'entrée et la valeur est un dictionnaire de mots clés qui sera transmis à la fonction en cours de test avec le fichier d'entrée. Si vous êtes intéressé, voici le code source et documentation . J'ai récemment joué avec l'idée de définir les options comme Unittests ici (nécessite uniquement la bibliothèque unittest intégrée) mais je ne sais pas si je l'aime.

3
Waylan

Réfléchissez si tout le contenu du fichier de configuration doit vraiment être testé.

Si seules plusieurs valeurs ou sous-chaînes doivent être vérifiées, préparez un modèle attendu pour cette configuration. Les emplacements testés seront marqués comme "variables" avec une syntaxe spéciale. Préparez ensuite une liste attendue distincte des valeurs des variables dans le modèle. Cette liste attendue peut être stockée dans un fichier séparé ou directement dans le code source.

Exemple pour le modèle:

ALLOWED_HOSTS = ['{Host}']
DEBUG = {debug}
DEFAULT_FROM_EMAIL = '{email}'

Ici, les variables du modèle sont placées entre accolades.

Les valeurs attendues peuvent ressembler à:

Host = www.example.com
debug = False
email = [email protected]

ou même comme une simple liste séparée par des virgules:

www.example.com, False, [email protected]

Ensuite, votre code de test peut produire le fichier attendu à partir du modèle en remplaçant les variables par les valeurs attendues. Et le fichier attendu est comparé au fichier réel.

La gestion séparée du modèle et des valeurs attendues présente l'avantage de permettre de nombreux tests de jeux de données à l'aide du même modèle.

Tester uniquement les variables

Une approche encore meilleure est que la méthode de génération de configuration ne produit que les valeurs nécessaires pour le fichier de configuration. Ces valeurs peuvent être facilement insérées dans le modèle par une autre méthode. Mais l'avantage est que le code de test peut comparer directement toutes les variables de configuration séparément et de manière claire.

Modèles

Bien qu'il soit facile de remplacer les variables par les valeurs nécessaires dans le modèle, il existe des bibliothèques de modèles prêtes, qui permettent de le faire en une seule ligne. Voici quelques exemples: Django , Jinja , Mako

1

Je pense que pytest-datafiles peut être d'une grande aide. Malheureusement, elle ne semble plus être maintenue. Pour le moment, cela fonctionne bien.

Voici un exemple simple tiré de la documentation:

import os
import pytest

@pytest.mark.datafiles('/opt/big_files/film1.mp4')
def test_fast_forward(datafiles):
    path = str(datafiles)  # Convert from py.path object to path (str)
    assert len(os.listdir(path)) == 1
    assert os.path.isfile(os.path.join(path, 'film1.mp4'))
    #assert some_operation(os.path.join(path, 'film1.mp4')) == expected_result

    # Using py.path syntax
    assert len(datafiles.listdir()) == 1
    assert (datafiles / 'film1.mp4').check(file=1)
0
Dror