ARTICLES
tox
By Valentin Bremond
pytest sous stéroïdes
Tout le monde connaît pytest. Ou plutôt : tout le monde devrait connaître pytest. Dans tous les cas, je ne vais pas expliquer ici ce que c’est, le Web entier s’en charge déjà très bien.
Le problème avec pytest, c’est les librairies. Pour lancer vos tests, vous devez être dans un virtualenv (ou avoir les bonnes librairies installées sur votre machine). Et ça, c’est relou. Relou pour vous, mais relou aussi pour toute personne qui voudra tester votre code.
tox vous permet de vous simplifier la vie pour les tests. Il va vous créer des virtualenv à la volée et tester votre code avec les librairies que vous aurez défini dans votre projet. Mieux, il vous permet aussi de lancer vos tests sous plusieurs versions de Python (vous voulez tester votre code en 2.6, 2.7, 3.4 et 3.6 ? Facile).
Je vous tease un peu : pour tester tout votre code sur toutes les versions de Python (sans les librairies installées), vous n’aurez plus qu’à faire :
1toxÇa vous tente ?
Exemple pratique
On va prendre un exemple tout simple d’une librairie qui va fournir quelques méthodes pour jouer avec des strings : une méthode uppercase_each_word, une count_characters, une autre split_comma et une dernière decorate.
Alors oui je sais, gnagna, ça sert à rien, le builtin le fait déjà, gnagna. OSEF, c’est un exemple.
Vous pouvez directement récupérer ce code source depuis GitHub : tox-example.
On aura donc un truc qui ressemble à ça (mettons qu’on appelle notre super lib “stringz”) :
1lib/
2 requirements.txt
3 setup.py
4 stringz/
5 __init__.py
6 tests/
7 test_stringz.py
8 tox.iniAvec comme fichiers :
requirements.txt :
1jinja2setup.py :
1import setuptools
2
3
4install_requires = [
5 "Jinja2",
6]
7
8setuptools.setup(
9 name="stringz",
10 version="1.0",
11 description="Best string library ever. Seriously.",
12
13 packages=setuptools.find_packages(),
14 include_package_data=True,
15
16 install_requires=install_requires,
17)stringz/__init__.py :
1import jinja2
2
3
4def uppercase_each_word(string):
5 """Uppercase the first letter of each word."""
6 if type(string) is not str:
7 raise ValueError("This is not a string")
8
9 return string.title()
10
11
12def count_characters(string):
13 """Return the amount of characters in the string."""
14 if type(string) is not str:
15 raise ValueError("This is not a string")
16
17 return len(string)
18
19
20def split_comma(string):
21 """Split the string at the comma and return the list."""
22 if type(string) is not str:
23 raise ValueError("This is not a string")
24
25 return string.split(',')
26
27
28def decorate(string):
29 """Add text around the string to "decorate" it."""
30 if type(string) is not str:
31 raise ValueError("This is not a string")
32
33 text = """Quelle phrase magnifique !
34Jugez-en par vous-meme : {{sentence}}
35N'est-ce pas ?"""
36
37 return jinja2.Environment().from_string(text).render(sentence=string)tests/test_stringz.py :
1import pytest
2
3from stringz import count_characters, uppercase_each_word, split_comma, decorate
4
5
6def test_uppercase_each_word():
7 assert uppercase_each_word('coucou les amis') == 'Coucou Les Amis'
8 assert uppercase_each_word('Deja Uppercase') == 'Deja Uppercase'
9
10 with pytest.raises(ValueError):
11 uppercase_each_word(123)
12
13
14def test_count_characters():
15 assert count_characters('quelques caracteres') == 19
16 assert count_characters('beaucoup plus de caracteres ici dirait-on') == 41
17
18 with pytest.raises(ValueError):
19 count_characters(None)
20
21
22def test_split_comma():
23 assert split_comma('Bonjour, cher ami') == ['Bonjour', ' cher ami']
24 assert split_comma('Ceci, va, etre, coupe, de partout') == [
25 'Ceci', ' va', ' etre', ' coupe', ' de partout'
26 ]
27
28 with pytest.raises(ValueError):
29 split_comma(["die!"])
30
31
32def test_decorate():
33 assert decorate('Au revoir.') == """Quelle phrase magnifique !
34Jugez-en par vous-meme : Au revoir.
35N'est-ce pas ?"""
36
37 with pytest.raises(ValueError):
38 decorate({"you": "may die"})tox.ini :
1[tox]
2envlist = py26, py27, py34, py35
3
4[testenv]
5deps =
6 pytest
7 -r{toxinidir}/requirements.txt
8
9commands =
10 pytestComme vous pouvez le constater ci-dessus, on va tester notre code sur Python 2.6, 2.7, 3.4 et 3.5 avec une dépendance sur jinja2.
Comment on teste sur toutes les versions ? Comme ça :
1toxVous notez comme c’est simple ?
Remarquez toutefois qu’étant donné que Pip ne peut pas vous télécharger votre interpréteur Python, si vous voulez tester sur plusieurs versions de Python, il faudra que vous les installiez vous-même sur votre machine (mais c’est très probable que vous ayiez déjà Python 2.7 et Python 3.5 installés de base sur votre distro).
Astuce pour les pressés : tox -e py27 pour ne tester que sur Python 2.7.
Vous aimez le code coverage ? Vous voulez savoir à peu près à quel pourcentage votre code est testé ? Rajoutez ça dans votre tox.ini :
1[tox]
2envlist = py26, py27, py34, py35
3
4[testenv]
5deps =
6 pytest
7 pytest-cov
8 -r{toxinidir}/requirements.txt
9
10commands =
11 pytest --cov={envsitepackagesdir}/stringzet admirez le résultat.
Ain’t nobody got time for that: detox
Vous aurez remarqué que comme tox doit générer des virtualenv pour chaque version de Python puis tester votre code, chaque version de Python est testée en séquentiel. Dans un sens, c’est pratique : dès que vous voyez du rouge, vous pouvez [Ctrl]+c pour annuler les tests et regarder où ça plante. Par contre, ça peut devenir très, très lent si vous avez une grosse base de code.
La solution ? detox. Comment ça marche ? Pareil :
1detoxdetox va simplement lancer tous vos tests sur vos différentes versions de Python, mais en parallèle (toutes les versions en même temps). Tout bêtement.
Notez la différence :
1$ time tox > /dev/null
2real 7.16
3user 6.38
4sys 0.76
5
6$ time detox > /dev/null
7real 3.36
8user 8.50
9sys 0.824 secondes gagnées sur notre tout petit projet. Imaginez sur un gros projet et 3 versions de Python.
Conclusion
tox, c’est génial. En une commande vous testez tout votre code, sur toutes les versions de Python et vous avez même du code coverage en bonus.
Je ne sais pas si vous souvenez de pex (lisez ça si vous ne connaissez pas, vous ne serez pas déçus), mais tox en est un formidable allié. Avec ces deux outils, vous êtes parés pour faire du bon code et du code autonome.