Ceci est l'article du 11ème jour du Calendrier de l'Avent Python Partie 2 2015.
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
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`).
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
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.
setup.cfg
.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)
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
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:{'Connection': 'keep-alive', 'User-Agent': 'python-requests/2.8.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*'}
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:{'Content-Type': 'application/json', 'Date': 'Thu, 10 Dec 2015 03:29:34 GMT', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Content-Length': '33', 'Access-Control-Allow-Credentials': 'true', 'Server': 'nginx'}
DEBUG:sample.client:2015-12-10 12:29:33,811:Body:{
"origin": "124.33.163.178"
}
</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>%
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
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.
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
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
Il existe diverses autres fonctions, donc si vous êtes intéressé, vous devriez lire le Document officiel.
Demain 12, c'est @shinyorke.
Recommended Posts