{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Des tests en Python\n", "\n", "Il sera question de tests unitaires en Python. \n", "On ne parlera pas de méthodes de développement ou de gestion de projet. Méthodes agiles, Extreme Programming, Test Driven Development (TDD) ce n'est pas le sujet, mais vous avez les mots clés vous pouvez toujours vous informer." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Le web est plein de beaux discours sur l'utilité des tests. Dans notre cas on se limitera à dire que les tests peuvent sauver des vies. \n", "L'intérêt principal des tests est de s'assurer qu'en modifiant un code existant on ne casse pas ce qui fonctionnait avant ou pire que les bugs qu'on avait résolus ne reviennent pas. On appelle ça des tests de non-régression. C'est *très* utile. \n", "\n", "Il nous faut des exemples. Pour cela on va s'appuyer sur le code de la classe `Word` et de ses classes filles." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import re\n", "\n", "class Word:\n", " \"\"\" Classe Word : définit un mot simple de la langue \"\"\"\n", "\n", " type = \"simple\"\n", "\n", " def __init__(self, *args):\n", " self.form, self.lemma, self.pos = args\n", "\n", " def is_inflected(self):\n", " if self.form != self.lemma:\n", " return True\n", " else:\n", " return False\n", "\n", " def magic_compare(self, other_word):\n", " diff = []\n", " for key in self.__dict__.keys():\n", " if self.__dict__[key] != other_word.__dict__[key]:\n", " diff.append(key)\n", " return diff\n", "\n", " def __str__(self):\n", " return \" \".join((self.form, self.lemma, self.pos))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class WordTreeTagger(Word):\n", " def __init__(self, formatted_str):\n", " \"\"\"\n", " import a formatted_str in treetagger format\n", " \"\"\"\n", " pattern = re.compile(\"(\\w+)\\t(\\w+)\\t(\\w+)\")\n", " res = pattern.search(formatted_str)\n", " self.form, self.lemma, self.pos = res.groups()\n", "\n", "class WordBrown(Word):\n", " def __init__(self, formatted_str):\n", " \"\"\"\n", " import a formatted_str in brown format\n", " \"\"\"\n", " pattern = re.compile(\"(\\w+)/(\\w+)/(\\w+)\")\n", " res = pattern.search(formatted_str)\n", " self.form, self.lemma, self.pos = res.groups()\n", "\n", "class WordSem(Word):\n", " def __init__(self, formatted_str):\n", " \"\"\"\n", " import a formatted_str in sem format (brown with no lemmas)\n", " \"\"\"\n", " pattern = re.compile(\"(\\w+)/(\\w+)\")\n", " res = pattern.search(formatted_str)\n", " self.form, self.pos = res.groups()\n", " self.lemma = \"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## `assert` sert à ça\n", "\n", "Normalement on met les instructions de tests dans un fichier distinct. \n", "L'idée ici est de tester toutes les valeurs possibles que l'on pourra recontrer pour tester la robustesse du code (oui comme dans CodinGame). On commence par une valeur standard, un truc facile puis on va chercher les trucs vicieux. \n", "Dit autrement le but est de faire planter le code pour pouvoir l'améliorer." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "obj_sem = WordSem(\"tests/NC\")\n", "assert obj_sem.lemma == \"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Il ne se passe rien, c'est que tout va bien." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "obj_sem = WordSem(\";/PONCT\")\n", "assert obj_sem.form == \";\"\n", "obj_sem_2 = WordSem(\"aujourd'hui/NC\")\n", "assert obj_sem_2.form == \"aujourd'hui\"\n", "obj_sem_3 = WordSem(\"13/12/2017/NC\")\n", "assert obj_sem_3.form == \"13/12/2017\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Blam ! `assert` lève une exception de type `AssertionError` quand la condition testée n'est pas remplie. Le programme est interrompu. \n", "Le problème ici est que seule la première instruction `assert` est testée, les deux restantes ne sont pas examinées. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## `unittest` les tests canal historique" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`unittest` est inspiré de `Junit` le framework de test qui a rendu les tests populaires, enfin non il les a popularisés.\n", "\n", "Pour utiliser `unittest` il faut écrire une classe dont le nom commence par Test. La classedevra aussi hériter de `unittest.TestCase`. \n", "\n", "Recopier le code suivant dans un fichier `test_word_sem.py` (les fichiers de test commencent systématiquement par `test_`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "import unittest\n", "from word import WordSem\n", "\n", "class TestWordSem(unittest.TestCase):\n", " def test_init_simple(self):\n", " obj_sem = WordSem(\"été/NC\")\n", " self.assertEqual(obj_sem.form, \";\")\n", " \n", " def test_init_ponct(self):\n", " obj_sem = WordSem(\";/PONCT\")\n", " self.assertEqual(obj_sem.form, \";\")\n", "\n", " def test_init_apostrophe(self):\n", " obj_sem = WordSem(\"aujourd'hui/NC\")\n", " self.assertEqual(obj_sem.form, \"aujourd'hui\")\n", "\n", " def test_init_slash(self):\n", " obj_sem = WordSem(\"13/12/2017/NC\")\n", " self.assertEqual(obj_sem.form, \"13/12/2017\")\n", " \n", "if __name__ == '__main__':\n", " unittest.main()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%run test_word_sem.py -v\n", "# python3 test_word_sem.py -v sur votre console shell" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1 erreur, 3 échecs et un succès, il reste du travail. Vous noterez que l'ouput est suffisamment détaillé pour comprendre exactement où se situent les erreurs. \n", "Je vous invite à lire la [doc de `unittest`](https://docs.python.org/3.6/library/unittest.html) pour en savoir plus sur :\n", " * les méthodes `assertQuelqueChose` du modules\n", " * les autres méthodes notamment `setUp` qui permet d'initialiser la classe de test avec les données nécessaires aux tests.\n", " * les conseils pour organiser vos fichiers de tests et pour les appeler" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Code et `pytest`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`unittest` c'est bien mais un peu fastidieux à mettre en oeuvre quand même. \n", "Il existe plusieurs environnements tests en Python mais la meilleure alternative à `unittest` c'est [`pytest`](http://docs.pytest.org/en/latest/). \n", "Avec `pytest` voici de que devient notre fichier de test : " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from word import WordSem\n", "\n", "def test_init_simple():\n", " obj_sem = WordSem(\"été/NC\")\n", " assert obj_sem.form == \"été\"\n", " \n", "def test_init_ponct():\n", " obj_sem = WordSem(\";/PONCT\")\n", " assert obj_sem.form == \";\"\n", "\n", "def test_init_apostrophe():\n", " obj_sem = WordSem(\"aujourd'hui/NC\")\n", " assert obj_sem.form == \"aujourd'hui\"\n", "\n", "def test_init_slash(self):\n", " obj_sem = WordSem(\"13/12/2017/NC\")\n", " assert obj_sem.form == \"13/12/2017\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "C'est tout de suite plus attrayant. \n", "Pour lancer les tests tapez la commande `pytest`. C'est tout ? Oui c'est tout." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pytest" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## `doctest`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Le module `doctest` permet d'insérer des tests dans les docstrings. Les tests ne seront pas dans un ou des fichiers distincts mais directement dans le code. \n", "Les tests sont écrits à la manière d'une session python dans le shell. \n", "Vous noterez l'appel au module à la fin du fichier" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class WordTreeTagger(Word):\n", " def __init__(self, formatted_str):\n", " \"\"\"\n", " import a formatted_str in treetagger format\n", " \"\"\"\n", " pattern = re.compile(\"(\\w+)\\t(\\w+)\\t(\\w+)\")\n", " res = pattern.search(formatted_str)\n", " self.form, self.lemma, self.pos = res.groups()\n", "\n", "class WordBrown(Word):\n", " def __init__(self, formatted_str):\n", " \"\"\"\n", " import a formatted_str in brown format\n", " \"\"\"\n", " pattern = re.compile(\"(\\w+)/(\\w+)/(\\w+)\")\n", " res = pattern.search(formatted_str)\n", " self.form, self.lemma, self.pos = res.groups()\n", "\n", "class WordSem(Word):\n", " def __init__(self, formatted_str):\n", " \"\"\"\n", " import a formatted_str in sem format (brown with no lemmas)\n", " \n", " >>> obj_sem = WordSem(\"été/NC\")\n", " >>> obj_sem.form\n", " 'été'\n", " \"\"\"\n", " pattern = re.compile(\"(\\w+)/(\\w+)\")\n", " res = pattern.search(formatted_str)\n", " self.form, self.pos = res.groups()\n", " self.lemma = \"\"\n", "\n", "if __name__ == \"__main__\":\n", " import doctest\n", " doctest.testmod()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "On appelle les tests avec la commande `python3 monficher.py` tout simplement, il ne se passe rien si les tests sont validés. Plus de détails avec l'option `-v`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class WordSem(Word):\n", " def __init__(self, formatted_str):\n", " \"\"\"\n", " import a formatted_str in sem format (brown with no lemmas)\n", " \n", " >>> obj_sem = WordSem(\"été/NC\")\n", " >>> obj_sem.form\n", " 'été'\n", " \n", " >>> obj_sem = WordSem(\";/PONCT\")\n", " >>> obj_sem.form\n", " ';'\n", "\n", " >>> obj_sem = WordSem(\"aujourd'hui/NC\")\n", " >>> obj_sem.form\n", " \"aujourd'hui\"\n", "\n", " >>> obj_sem = WordSem(\"13/12/2017/NC\")\n", " >>> obj_sem.form\n", " '13/12/2017'\n", "\n", " \"\"\"\n", " pattern = re.compile(\"(\\w+)/(\\w+)\")\n", " res = pattern.search(formatted_str)\n", " self.form, self.pos = res.groups()\n", " self.lemma = \"\"\n", "\n", "if __name__ == \"__main__\":\n", " import doctest\n", " doctest.testmod()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pour des tests simples, comme les nôtres ici, `doctest` est parfait. C'est léger, pas de fichier distinct et en plus les tests permettent de documenter le code et son utilisation. \n", "`doctest` ne convient pas par contre aux tests qui nécessitent une initialisation lourde (connexion à une base de données par ex.). On ne trouve pas d'équivalent de la méthode `setUp` de `unittest` ou des fixtures de `pytest` dont on n'a pas parlé. \n", "Rien ne vous empêche d'utiliser `doctest` et `pytest` par exemple." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Il ne nous reste plus qu'à corriger le code pour passer les tests maintenant." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.0" } }, "nbformat": 4, "nbformat_minor": 1 }