Python et les tests (nose1, nose2, python -m unittest)

Quand j'ai commencé à développer en Python, j'ai très rapidement utilisé Nose pour exécuter mes tests unitaires.

Je souhaite dans ce billet faire un petit point sur les différentes versions de Nose (Nose version 1, Nose version 2) ainsi que la commande discover de unittest2.

Nose1, Nose2 et python -m unittest discover ?

Ces trois outils ont pour fonction de trouver (discover) les tests d'un projet, de les exécuter et d'afficher le bilan de l'exécution des tests.

Les auteurs de ces outils

Nose1 et Nose2 sont principalement développés par Jason Pellerin.

Unittest est principalement développé par Michael Foord.

Utilisation de Nose

Je vais commencer par un très court exemple d'utilisation de Nose.

Ce que j'ai tout de suite aimé avec Nose, c'est que l'on peut commencer par écrire des tests très simples. Exemple, d'un fichier nommé test1.py :

def test_foo():
    a = 1
    assert a == 1

def test_bar():
    b = 'data'
    assert b == 'data'

Lancement de la commande nose :

$ bin/nosetests -v
test1.test_foo ... ok
test1.test_bar ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK

Dans cet exemple, tout est minimaliste, pas de classes du type TestCase, de TestSuite

Pour débuter un projet, cette simplicité est bien pratique même si par la suite il est bon de migrer peu à peu vers une syntax Unittest (toujours exécutable par nose) afin de tirer profit par exemple des fonctionnalités suivantes :

Exemple :

import unittest

class MyTest(unittest.TestCase):
    def setUp(self):
        self.session = 'foobar'

    def tearDown(self):
        self.session = None

    def test_foo(self):
        a = 1
        self.assertEqual(a, 1)

    def test_bar(self):
        b = 'data' + self.session
        self.assertEqual(b, 'datafoobar')

L'exécution de ce test avec nose2 :

$ bin/nose2 -s tests/ -v
test_bar (test_basic.MyTest) ... ok
test_foo (test_basic.MyTest) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

En 2013, est-ce que je dois utiliser Nose1 ou Nose2 ?

Tout d'abord, un peu d'histoire :

Pour simplifier, un des objectifs de Nose2 est de tirer parti des capacités d'extensions de unittest2 (je ne peux pas en dire plus, je ne connais pas le code interne de Nose).
D'après l'auteur, le code de Nose2 est plus petit et plus simple à maintenir. Illustration :
  • le coeur de Nose1 fait environ 3000 lignes de code
  • le coeur de Nose2 fait environ 1300 lignes de code

Une page de la documentation de nose2 qui liste les différences entre nose, nose2 et unittest.

Début 2013, j'ai demandé à l'auteur de Nose2 si je devais actuellement utiliser Nose1 ou Nose2 : « In 2013, I need to use nose1 or nose2 ? What is the future of nose ? ».
Pour résumer sa réponse :
  • Nose1 est en mode maintenance
  • L'avenir est du côté de Nose2
  • Jason Pellerin utilise de plus en plus Nose2 à la place de Nose1

Suite à cela, j'ai pris la décision d'utiliser quand c'est possible Nose2 à la place de Nose1.

Différences d'utilisation entre Nose2 et Nose1

En dehors du fait que l'exécutable de Nose1 se nomme nosetests et que celui de Nose2 se nomme simplement nose2, voici quelques différences d'utilisation que j'ai remarquées :

  • L'option -f permet d'arrêter les tests dès que nose rencontre une erreur (cette option était -x avec nose1)

  • L'affichage de la sortie standard est activé par défaut dans nose2. Avec nose1 il faut utiliser l'option -s pour activer l'affichage de la sortie standard.

  • Changement de syntaxe pour le lancement direct d'un ou plusieurs tests :

    Avec Nose1 :

    $ nosetests tests/test1.py:test_foo
    

    La même chose en Nose2 :

    $ nosetests tests.test1.test_foo
    
    À noter qu'avec nose1 le chemin du test à exécuter était relatif au dossier courant.
    Avec nose2 c'est relatif au dossier racine des tests.

Voici plusieurs méthodes pour indiquer la racine des tests :

  • par défaut c'est le dossier courant

  • il est possible d'indiquer le dossier racine via l'option -s, exemple :

    $ nose2 -s tests/
    
  • il est possible de configurer le dossier racine via le fichier de configuration unittest.cfg (fonctionnalité disponible dans le HEAD du projet ou dans la prochaine release), exemple :

    [unittest]
    start-dir=tests
    

En dehors de cela, j'utilise l'option verbose -v mais rien de particulier à ce niveau.

Utilisation de Discover

Depuis Python 2.7, il est possible de lancer les tests avec la fonction discover qui est directement intégré dans unittest (la version 2).

À tort ou à raison, je n'ai pratiquement jamais utilisé cette fonctionnalité discover.

Exemple :

$ python -m unittest discover -s tests
.............
----------------------------------------------------------------------
Ran 13 tests in 9.969s

D'après ce que j'ai pu observer, python -m unittest discover utilise les mêmes noms d'options que nose2.

Il est aussi possible de lancer directement un test, exemples :

$ python -m unittest test_basic
$ python -m unittest test_basic.TestClass
$ python -m unittest test_basic.TestClass.test_method

À noter que la syntaxe ci-dessous ne permet pas de spécifier le dossier racine des tests.

Pour le moment, l'avantage que je trouve à nose2 par rapport à python -m unittest est le support du fichier de configuration unittest.cfg.

Par contre, je n'ai pas testé s'il est possible d'activer le contôle de couverture de code avec python -m unittest.

Où placer ses tests

Une des premières questions que je me suis posées lorsque j'ai commencé à faire des tests unitaires en python, c'est où placer mes tests.

Deux choix :

  1. soit placer les tests à la racine du projet à côté des modules de l'application ou de la librairie, exemple :

    .
    ├── README.txt
    ├── myproject
    │   ├── __init__.py
    │   └── main.py
    ├── setup.py
    ├── tests
    │   └── test_basic.py
    └── unittest.cfg
    
  2. soit placer les tests dans le dossier qui contient les modules de l'application ou de la librairie, exemple :

    .
    ├── README.txt
    ├── myproject
    │   ├── __init__.py
    │   ├── main.py
    │   └── tests
    │       ├── __init__.py
    │       └── test_basic.py
    └── setup.py
    

J'ai personnellement une préférence pour le choix numéro 1. Pourquoi :

  • pour moi les tests ne font pas partie du coeur du code source de ma librairie ou de mon application
  • je trouve que cela n'a pas de sens de faire import myproject.tests.TestCase

Si l'on regarde de nombreux projets python, les deux solutions sont utilisées même si j'ai l'impression qu'il y a une petite domination pour la solution numéro 2.

Pour le moment, je n'ai pas trouvé d'argument absolu en faveur de l'une des deux solutions.

La suite...

Il y a encore deux sujets que je souhaitais aborder dans ce billet :

Ces deux sujets seront sans doute développés dans deux prochains billets.

blog comments powered by Disqus

Mes flux :