[PYTHON] Pytest à traction inversée

Ceci est l'article du 11ème jour du Calendrier de l'Avent Python Partie 2 2015.

introduction

Cet article résume les conseils que j'ai obtenus lors de l'utilisation de pytest en format inversé. De plus, un exemple de projet (confirmé avec Python 3.5.0 / pytest 2.8.4) contenant le contenu de cet article est placé dans le référentiel suivant, veuillez donc vous y référer également. https://github.com/FGtatsuro/pytest_sample

Caractéristiques de pytest

pytest est, comme son nom l'indique, une bibliothèque de tests écrite en Python. Des bibliothèques similaires incluent unittest et nose.

Comme je ne suis pas familier avec les deux outils ci-dessus, je ne peux pas faire une évaluation par rapport à eux, mais j'ai personnellement estimé que les points suivants étaient caractéristiques.

--Assaut avec l'instruction d'assert standard Python sans définir votre propre méthode d'assert (ex. ʻAsertEquals`).

Conseils pytest

Spécifiez les valeurs par défaut des options

Vous pouvez spécifier la valeur par défaut de l'option donnée à pytest dans setup.cfg.

setup.cfg


#Les options qui peuvent être spécifiées sont py.test --Voir l'aide
[pytest]
addopts = -v

(Référence) http://pytest.org/latest/customize.html?highlight=setup%20cfg#adding-default-options

Définir un délai pour le test

Le pytest lui-même n'a pas de fonction de timeout, mais il peut être supporté par le plug-in pytest-timeout.

pytest-Installation du plug-in timeout


$ pip install pytest-timeout

Il existe plusieurs façons de définir le délai d'expiration, qui peuvent être combinées comme suit.

Délai d'expiration par défaut


[pytest]
addopts = -v
timeout = 5

Spécification du délai d'expiration par annotation


import time

@pytest.mark.timeout(10)
def test_timeout():
    time.sleep(8)

Afficher les journaux d'exécution sur la sortie standard

Lorsque l'accès HTTP se produit dans un test d'intégration, etc., il est pratique de pouvoir confirmer la requête / réponse HTTP au moment de l'exécution du test dans la sortie standard (dans de nombreux cas, au moins lors de la mise en œuvre du test). En prenant requêtes comme exemple, l'objectif peut être atteint en définissant un gestionnaire comme suit.

Exemples d'implémentation de gestionnaire pour les requêtes


import requests
# _la journalisation est un module auto-défini
from ._logging import get_logger

logger = get_logger(__name__)

class ResponseHandler(object):

    def __call__(self, resp, *args, **kwargs):
        logger.debug('### Request ###')
        logger.debug('Method:{0}'.format(resp.request.method))
        logger.debug('URL:{0}'.format(resp.request.url))
        logger.debug('Header:{0}'.format(resp.request.headers))
        logger.debug('Body:{0}'.format(resp.request.body))
        logger.debug('### Response ###')
        logger.debug('Status:{0}'.format(resp.status_code))
        logger.debug('Header:{0}'.format(resp.headers))
        logger.debug('Body:{0}'.format(resp.text))

class HttpBinClient(object):
    '''
    The client for https://httpbin.org/
    '''

    base = 'https://httpbin.org'

    def __init__(self):
        self.session = requests.Session()
        #Enregistrer le gestionnaire créé
        # http://docs.python-requests.org/en/latest/user/advanced/?highlight=response#event-hooks
        self.session.hooks = {'response': ResponseHandler()}
    
    def ip(self):
        return self.session.get('{0}/ip'.format(self.base))

Cependant, avec les paramètres par défaut de pytest, le contenu de la sortie standard lorsque le test est exécuté est capturé par pytest (utilisé pour rapporter les résultats du test), de sorte que la sortie du gestionnaire ne peut pas être confirmée telle quelle. Pour vérifier, vous devez définir la valeur de l'option capture sur no.

--capture=Exemple d'exécution de non


$ py.test --capture=no
=========================================================================================== test session starts ============================================================================================
...
tests/test_calc.py::test_add
PASSED
tests/test_client.py::test_ip
DEBUG:sample.client:2015-12-10 11:26:17,265:### Request ###
DEBUG:sample.client:2015-12-10 11:26:17,265:Method:GET
DEBUG:sample.client:2015-12-10 11:26:17,265:URL:https://httpbin.org/ip
DEBUG:sample.client:2015-12-10 11:26:17,265:Header:{'Accept-Encoding': 'gzip, deflate', 'Connection': 'keep-alive', 'User-Agent': 'python-requests/2.8.1', 'Accept': '*/*'}
DEBUG:sample.client:2015-12-10 11:26:17,265:Body:None
DEBUG:sample.client:2015-12-10 11:26:17,265:### Response ###
DEBUG:sample.client:2015-12-10 11:26:17,265:Status:200
DEBUG:sample.client:2015-12-10 11:26:17,265:Header:{'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Server': 'nginx', 'Access-Control-Allow-Credentials': 'true', 'Content-Length': '33', 'Connection': 'keep-alive', 'Date': 'Thu, 10 Dec 2015 02:26:17 GMT'}
DEBUG:sample.client:2015-12-10 11:26:17,315:Body:{
  "origin": xxxxx
}
...

S'il est difficile de spécifier l'option à chaque fois, il est bon de spécifier la valeur par défaut.

Valeur par défaut pour l'option de capture


[pytest]
addopts = -v --capture=no
timeout = 5

(Référence) http://pytest.org/latest/capture.html?highlight=capture

Travailler avec Jenkins

Avec l'option junit-xml, vous pouvez générer un rapport de test au format XUnit dans un fichier spécifié. Cela facilite le travail avec Jenkins.

Rapport de sortie au format XUnit


[pytest]
addopts = -v --capture=no --junit-xml=results/results.xml
timeout = 5

Lors de la vérification des résultats des tests avec Jenkins, il est pratique de pouvoir vérifier les méthodes de test individuelles et la sortie standard à ce moment-là. Comme mentionné ci-dessus, si la valeur par défaut de l'option capture est définie sur no, le rapport de test n'inclut pas la sortie standard, il est donc préférable d'écraser la valeur lors de l'exécution.

--capture=Spécifiez sys lors de l'exécution


$ py.test --capture=sys
=========================================================================================== test session starts ============================================================================================
...
tests/test_calc.py::test_add PASSED
#Requête HTTP/Aucun journal de réponse
tests/test_client.py::test_ip PASSED
tests/test_timeout.py::test_timeout PASSED
...

#Requête HTTP pour rapport de test/Comprend le journal des réponses
$ cat results/results.xml
<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="0" name="pytest" skips="0" tests="3" time="9.110"><testcase classname="tests.test_calc" file="tests/test_calc.py" line="5" name="test_add" time="0.00024390220642089844"><system-out>
</system-out></testcase><testcase classname="tests.test_client" file="tests/test_client.py" line="7" name="test_ip" time="0.9390749931335449"><system-out>
DEBUG:sample.client:2015-12-10 12:29:33,753:### Request ###
DEBUG:sample.client:2015-12-10 12:29:33,754:Method:GET
DEBUG:sample.client:2015-12-10 12:29:33,754:URL:https://httpbin.org/ip
DEBUG:sample.client:2015-12-10 12:29:33,754:Header:{&apos;Connection&apos;: &apos;keep-alive&apos;, &apos;User-Agent&apos;: &apos;python-requests/2.8.1&apos;, &apos;Accept-Encoding&apos;: &apos;gzip, deflate&apos;, &apos;Accept&apos;: &apos;*/*&apos;}
DEBUG:sample.client:2015-12-10 12:29:33,754:Body:None
DEBUG:sample.client:2015-12-10 12:29:33,754:### Response ###
DEBUG:sample.client:2015-12-10 12:29:33,754:Status:200
DEBUG:sample.client:2015-12-10 12:29:33,754:Header:{&apos;Content-Type&apos;: &apos;application/json&apos;, &apos;Date&apos;: &apos;Thu, 10 Dec 2015 03:29:34 GMT&apos;, &apos;Connection&apos;: &apos;keep-alive&apos;, &apos;Access-Control-Allow-Origin&apos;: &apos;*&apos;, &apos;Content-Length&apos;: &apos;33&apos;, &apos;Access-Control-Allow-Credentials&apos;: &apos;true&apos;, &apos;Server&apos;: &apos;nginx&apos;}
DEBUG:sample.client:2015-12-10 12:29:33,811:Body:{
  &quot;origin&quot;: &quot;124.33.163.178&quot;
}

</system-out></testcase><testcase classname="tests.test_timeout" file="tests/test_timeout.py" line="8" name="test_timeout" time="8.001494884490967"><system-out>
</system-out></testcase></testsuite>% 

Exécutez le test via setuptools

En liant avec setuptools, vous pouvez exécuter des tests sans polluer l'environnement d'exécution (sans changer le contenu de = pip freeze). Je pense que cela est utile dans le sens où vous pouvez éviter des problèmes inutiles si vous avez la possibilité d'exécuter le test autrement que vous-même et que l'exécuteur n'utilise pas suffisamment Python pour couper l'environnement avec virtualenv. Un exemple de setup.py est présenté ci-dessous.

setup.py(Coopération entre les outils de configuration et pytest)


import os
import sys

from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand

#Implémentation d'une commande pour exécuter Pytest
class PyTest(TestCommand):

    #Lors de la spécification des options de test pytest--pytest-args='{options}'Utiliser
    user_options = [
        ('pytest-args=', 'a', 'Arguments for pytest'),
    ]

    def initialize_options(self):
        TestCommand.initialize_options(self)
        self.pytest_target = []
        self.pytest_args = []

    def finalize_options(self):
        TestCommand.finalize_options(self)
        self.test_args = []
        self.test_suite = True

    def run_tests(self):
        import pytest
        errno = pytest.main(self.pytest_args)
        sys.exit(errno)

version = '0.1'

# setup.Besoin d'importer pytest dans py
setup_requires=[
    'pytest'
]
install_requires=[
    'requests',
]
tests_require=[
    'pytest-timeout',
    'pytest'
]

setup(name='pytest_sample',
      ...
      setup_requires=setup_requires,
      install_requires=install_requires,
      tests_require=tests_require,
      # 'test'Est associé à "commande pour exécuter Pytest"
      cmdclass={'test': PyTest},
)

Après avoir changé setup.py, vous pouvez lancer le test avec python setup.py test

Exécution des tests via les outils de configuration


$ python setup.py test
...
tests/test_calc.py::test_add
PASSED
tests/test_client.py::test_ip
DEBUG:sample.client:2015-12-10 12:54:20,426:### Request ###
DEBUG:sample.client:2015-12-10 12:54:20,426:Method:GET
DEBUG:sample.client:2015-12-10 12:54:20,426:URL:https://httpbin.org/ip
DEBUG:sample.client:2015-12-10 12:54:20,426:Header:{'Connection': 'keep-alive', 'User-Agent': 'python-requests/2.8.1', 'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate'}
DEBUG:sample.client:2015-12-10 12:54:20,426:Body:None
DEBUG:sample.client:2015-12-10 12:54:20,426:### Response ###
DEBUG:sample.client:2015-12-10 12:54:20,426:Status:200
DEBUG:sample.client:2015-12-10 12:54:20,426:Header:{'Server': 'nginx', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true', 'Connection': 'keep-alive', 'Content-Length': '33', 'Date': 'Thu, 10 Dec 2015 03:54:20 GMT', 'Content-Type': 'application/json'}
DEBUG:sample.client:2015-12-10 12:54:20,484:Body:{
  "origin": "124.33.163.178"
}

PASSED
tests/test_timeout.py::test_timeout
PASSED
...

Si vous voulez donner des options pytest, utilisez l'option --pytest-args définie dans l'implémentation ci-dessus.

Exécution des tests via les outils de configuration(Avec options)


$ python setup.py test --pytest-args='--capture=sys'
...
tests/test_calc.py::test_add PASSED
tests/test_client.py::test_ip PASSED
tests/test_timeout.py::test_timeout PASSED
...

(Référence) http://pytest.org/latest/goodpractises.html

(Supplément) Lorsque j'ai relu le document en écrivant ceci, j'ai découvert qu'il existe une bibliothèque qui fait la même chose. Il peut être bon de l'utiliser. https://pypi.python.org/pypi/pytest-runner

Exécuter des tests en parallèle

Le plug-in pytest-xdist vous permet d'exécuter des tests en parallèle.

Exécution parallèle de tests


#Exécution parallèle en 2 processus
$ py.test -n 2 

Puisqu'il sera exécuté dans un processus séparé, la dépendance de la bibliothèque doit être résolue à l'avance dans l'environnement dans lequel elle est exécutée. Par conséquent, il n'est pas compatible avec l'exécution via setuptools, qui résout les dépendances au moment de l'exécution.

Réexécuter uniquement les tests qui ont échoué lors de l'exécution précédente

L'option lf vous permet de réexécuter uniquement les tests qui ont échoué lors de l'exécution précédente.

Les informations sur les tests échoués sont enregistrées dans .cache / v / cache / lastfailed directement sous le répertoire où le test a été exécuté. Si vous souhaitez travailler avec d'autres outils (ex. Réexécuter uniquement en cas d'échec des tests), vous pouvez vous référer directement à ce fichier.

$ py.test --capture=sys
collected 3 items
...
tests/test_calc.py::test_add PASSED
tests/test_client.py::test_ip PASSED
tests/test_timeout.py::test_timeout FAILED
...
#Informations sur les tests échoués
$ cat .cache/v/cache/lastfailed
{
  "tests/test_timeout.py::test_timeout": true
}

#Correctif de test...
 
#Réexécuter uniquement les tests ayant échoué
$ py.test --capture=sys --lf

Filtrer les tests à exécuter

pytest peut filtrer les tests à exécuter selon différentes conditions.

Tout d'abord, de la méthode de spécification du module / méthode au flux.

module/Spécification de la méthode


#Spécifiez le module
$ py.test tests/test_calc.py
#Spécifiez une méthode dans le module
$ py.test tests/test_calc.py::test_add

Vous pouvez également exécuter uniquement les modules / méthodes qui correspondent à la chaîne

Filtrage par correspondance de chaîne


# 'calc'Module contenant la chaîne/Exécutez la méthode
$ py.test -k calc

Vous pouvez également filtrer par la marque du décorateur. Puisqu'il prend en charge «et / ou», il est possible de spécifier des conditions telles que avec / sans plusieurs marques.

Filtrage par marque


#Marquage: pytest.mark.<Toute chaîne de caractères peut être spécifiée>
@pytest.mark.slow
@pytest.mark.httpaccess
def test_ip():
    ...

@pytest.mark.slow
@pytest.mark.timeout(10)
def test_timeout():
    ...

# @pytest.mark.Exécuter des tests avec lent
$ py.test -m slow
# @pytest.mark.slow/@pytest.mark.Exécutez un test avec les deux httpaccess
$ py.test -m 'slow and httpaccess'
# @pytest.mark.C'est lent, [email protected]écuter des tests sans httpaccess
$ py.test -m 'slow and not httpaccess'

(Référence) https://pytest.org/latest/example/markers.html#mark-examples

en conclusion

Il existe diverses autres fonctions, donc si vous êtes intéressé, vous devriez lire le Document officiel.

Demain 12, c'est @shinyorke.

Recommended Posts

Pytest à traction inversée
Référence inversée Sympy
Mémo inversé Pandas
Référence inversée Luigi
pytest
Conseils de configuration du serveur Pull inversé
Mémo inversé de l'écran de gestion Django
Référence d'inversion de bibliothèque de date / heure Python
mémo pytest
résumé pytest
Traitement asynchrone en Python: référence inverse asyncio