From 21d427837389d2971aa3a2c0edbe1bedcc190a31 Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Fri, 3 Jun 2016 14:16:10 +0200 Subject: [PATCH] models and tests --- source/index.rst | 1 + source/intro/03-before-going-further.rst | 3 + source/models.rst | 2 +- source/models/models.rst | 37 ++---- source/tests.rst | 143 ++++++++++++++++++++++- 5 files changed, 153 insertions(+), 33 deletions(-) diff --git a/source/index.rst b/source/index.rst index 29de2a3..f0708a5 100644 --- a/source/index.rst +++ b/source/index.rst @@ -15,6 +15,7 @@ Contents: intro specs models + tests mvc forms auth diff --git a/source/intro/03-before-going-further.rst b/source/intro/03-before-going-further.rst index ccb91b5..9f6e2f6 100644 --- a/source/intro/03-before-going-further.rst +++ b/source/intro/03-before-going-further.rst @@ -49,12 +49,15 @@ Attention que celle-ci ne permet pas de vérifier que le code est **bien** test [run] branch = True omit = ../*migrations* + plugins = + django_coverage_plugin [report] ignore_errors = True [html] directory = coverage_html_report + .. code-block:: shell diff --git a/source/models.rst b/source/models.rst index 9fabae0..4b8e529 100644 --- a/source/models.rst +++ b/source/models.rst @@ -35,7 +35,7 @@ Cela nous donne ceci: Les classes sont créées, mais vides. Entrons dans les détails. -[Ajouter pourquoi on hérite de ``models.Model``, etc.) +.. todo:: Ajouter pourquoi on hérite de ``models.Model``, etc. .. include:: models/models.rst diff --git a/source/models/models.rst b/source/models/models.rst index 63c37a1..e2d521d 100644 --- a/source/models/models.rst +++ b/source/models/models.rst @@ -82,9 +82,9 @@ A nouveau, que peut-on constater ? * Comme cité ci-dessus, chaque champ possède des attributs spécifiques. Le champ ``DecimalField`` possède par exemple les attributs ``max_digits`` et ``decimal_places``, qui nous permettra de représenter une valeur comprise entre 0 et plus d'un milliard (avec deux chiffres décimaux). * L'ajout d'un champ de type ``ImageField`` nécessite l'installation de ``pillow`` pour la gestion des images. Nous l'ajoutons donc à nos pré-requis, dans le fichier ``requirements/base.txt``. -******* +***** Parts -******* +***** Les parts ont besoins des propriétés suivantes: @@ -107,34 +107,15 @@ Elles permettent à un utilisateur de participer au souhait émis par un autre u class WishPart(models.Model): wish = models.ForeignKey(Wish) - user = models.ForeignKey(User) - unknown_user = models.ForeignKey(UnknownUser) - comment = models.TextField() - done_at = models.DateTimeField() + user = models.ForeignKey(User, null=True) + unknown_user = models.ForeignKey(UnknownUser, null=True) + comment = models.TextField(null=True, blank=True) + done_at = models.DateTimeField(auto_now_add=True) La classe ``User`` référencée au début du snippet correspond à l'utilisateur géré par Django. Cette instance est accessible à chaque requête transmise au serveur, et est accessible grâce à l'objet ``request.user``, transmis à chaque fonction ou *Class-based-view*. C'est un des avantages d'un framework tout intégré: il vient *batteries-included* et beaucoup de détails ne doivent pas être pris en compte. Pour le moment, nous nous limiterons à ceci. Par la suite, nous verrons comment améliorer la gestion des profils utilisateurs, comment y ajouter des informations et comment gérer les cas particuliers. La classe ``UnknownUser`` permet de représenter un utilisateur non enregistré sur le site et est définie au point suivant. -Maintenant que la classe ``Part`` est définie, il nous est également possible de calculer le pourcentage d'avancement pour la réalisation d'un souhait. Pour cela, il nous suffit d'ajouter une nouvelle méthode au niveau de la classe ``Wish``, qui va calculer le nombre de parts déjà promises, et nous donnera l'avancement par rapport au nombre total de parts disponibles: - -.. code-block:: python - - class Wish(models.Model): - - [...] - - @property - def percentage(self): - """ - Calcule le pourcentage de complétion pour un élément. - """ - number_of_linked_parts = Part.objects.filter(wish=self).count() - total = self.number_of_parts * self.numbers_available - percentage = (number_of_linked_parts / total) - return percentage * 100 - -L'attribut ``@property`` va nous permettre d'appeler directement la méthode ``percentage()`` comme s'il s'agissait d'une propriété de la classe, au même titre que les champs ``number_of_parts`` ou ``numbers_available``. Attention que ce type de méthode fera un appel à la base de données à chaque appel. Il convient de ne pas surcharger ces méthodes de connexions à la base: sur de petites applications, ce type de comportement a très peu d'impacts. Ce n'est plus le cas sur de grosses applications ou sur des méthodes fréquemment appelées. Il convient alors de passer par un mécanisme de **cache**, que nous aborderons plus loin. ********************* Utilisateurs inconnus @@ -144,15 +125,15 @@ Pour chaque réalisation d'un souhait par quelqu'un, il est nécessaire de sauve * un identifiant * un nom - * une adresse email + * une adresse email. Cette adresse email sera unique dans notre base de données, pour ne pas créer une nouvelle occurence si un même utilisateur participe à la réalisation de plusieurs souhaits. -Ce qui donne après implémentation: +Ceci nous donne après implémentation: .. code-block:: python class UnkownUser(models.Model): name = models.CharField(max_length=255) - email = models.CharField(max_length=255) + email = models.CharField(email = models.CharField(max_length=255, unique=True) diff --git a/source/tests.rst b/source/tests.rst index 4817992..9e1115e 100644 --- a/source/tests.rst +++ b/source/tests.rst @@ -6,7 +6,21 @@ Tests unitaires Méthodologies ************* -(Copié/collé à partir de `ce lien `_): +Pourquoi s'ennuyer à écrire des tests? +====================================== + +Traduit grossièrement depuis un article sur `https://medium.com `_: + + Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont + + Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi: + + 1. Aide au design: écrire des tests avant d'écrire le code vous donnera une meilleure perspective sur le design à appliquer aux API. + 2. Documentation (pour les développeurs): chaque description d'un test + 3. Tester votre compréhension en tant que développeur: + 4. Assurance qualité: des tests, + 5. + Why Bother with Test Discipline? ================================ @@ -29,8 +43,129 @@ Unit tests don’t need to be twisted or manipulated to serve all of those broad 1. What component aspect are you testing? 2. What should the feature do? What specific behavior requirement are you testing? -************** -Quelques liens -************** + +Couverture de code +================== + +On a vu au chapitre 1 qu'il était possible d'obtenir une couverture de code, c'est-à-dire un pourcentage. + +Comment tester ? +================ + +Il y a deux manières d'écrire les tests: soit avant, soit après l'implémentation. Oui, idéalement, les tests doivent être écrits à l'avance. Entre nous, on ne va pas râler si vous faites l'inverse, l'important étant que vous le fassiez. Une bonne métrique pour vérifier l'avancement des tests est la couverture de code. + +Pour l'exemple, nous allons écrire la fonction ``percentage_of_completion`` sur la classe ``Wish``, et nous allons spécifier les résultats attendus avant même d'implémenter son contenu. Prenons le cas où nous écrivons la méthode avant son test: + +.. code-block:: python + + class Wish(models.Model): + + [...] + + @property + def percentage_of_completion(self): + """ + Calcule le pourcentage de complétion pour un élément. + """ + number_of_linked_parts = WishPart.objects.filter(wish=self).count() + total = self.number_of_parts * self.numbers_available + percentage = (number_of_linked_parts / total) + return percentage * 100 + +Lancez maintenant la couverture de code. Vous obtiendrez ceci: + +.. code-block:: shell + + $ coverage run --source "." src/manage.py test wish + $ coverage report + + Name Stmts Miss Branch BrPart Cover + ------------------------------------------------------------------ + src\gwift\__init__.py 0 0 0 0 100% + src\gwift\settings\__init__.py 4 0 0 0 100% + src\gwift\settings\base.py 14 0 0 0 100% + src\gwift\settings\dev.py 8 0 2 0 100% + src\manage.py 6 0 2 1 88% + src\wish\__init__.py 0 0 0 0 100% + src\wish\admin.py 1 0 0 0 100% + src\wish\models.py 36 5 0 0 88% + ------------------------------------------------------------------ + TOTAL 69 5 4 1 93% + +Si vous générez le rapport HTML avec la commande ``coverage html`` et que vous ouvrez le fichier ``coverage_html_report/src_wish_models_py.html``, vous verrez que les méthodes en rouge ne sont pas testées. +*A contrario*, la couverture de code atteignait **98%** avant l'ajout de cette nouvelle méthode. + +Pour cela, on va utiliser un fichier ``tests.py`` dans notre application ``wish``. *A priori*, ce fichier est créé automatiquement lorsque vous initialisez une nouvelle application. + +.. code-block:: shell + + from django.test import TestCase + + class TestWishModel(TestCase): + def test_percentage_of_completion(self): + """ + Vérifie que le pourcentage de complétion d'un souhait + est correctement calculé. + + Sur base d'un souhait, on crée quatre parts et on vérifie + que les valeurs s'étalent correctement sur 25%, 50%, 75% et 100%. + """ + wishlist = Wishlist(name='Fake WishList', + description='This is a faked wishlist') + wishlist.save() + + wish = Wish(wishlist=wishlist, + name='Fake Wish', + description='This is a faked wish', + number_of_parts=4) + wish.save() + + part1 = WishPart(wish=wish, comment='part1') + part1.save() + self.assertEqual(25, wish.percentage_of_completion) + + part2 = WishPart(wish=wish, comment='part2') + part2.save() + self.assertEqual(50, wish.percentage_of_completion) + + part3 = WishPart(wish=wish, comment='part3') + part3.save() + self.assertEqual(75, wish.percentage_of_completion) + + part4 = WishPart(wish=wish, comment='part4') + part4.save() + self.assertEqual(100, wish.percentage_of_completion) + +L'attribut ``@property`` sur la méthode ``percentage_of_completion()`` va nous permettre d'appeler directement la méthode ``percentage_of_completion()`` comme s'il s'agissait d'une propriété de la classe, au même titre que les champs ``number_of_parts`` ou ``numbers_available``. Attention que ce type de méthode contactera la base de données à chaque fois qu'elle sera appelée. Il convient de ne pas surcharger ces méthodes de connexions à la base: sur de petites applications, ce type de comportement a très peu d'impacts, mais ce n'est plus le cas sur de grosses applications ou sur des méthodes fréquemment appelées. Il convient alors de passer par un mécanisme de **cache**, que nous aborderons plus loin. + +En relançant la couverture de code, on voit à présent que nous arrivons à 99%: + +.. code-block:: shell + + $ coverage run --source='.' src/manage.py test wish; coverage report; coverage html; + . + ---------------------------------------------------------------------- + Ran 1 test in 0.006s + + OK + Creating test database for alias 'default'... + Destroying test database for alias 'default'... + Name Stmts Miss Branch BrPart Cover + ------------------------------------------------------------------ + src\gwift\__init__.py 0 0 0 0 100% + src\gwift\settings\__init__.py 4 0 0 0 100% + src\gwift\settings\base.py 14 0 0 0 100% + src\gwift\settings\dev.py 8 0 2 0 100% + src\manage.py 6 0 2 1 88% + src\wish\__init__.py 0 0 0 0 100% + src\wish\admin.py 1 0 0 0 100% + src\wish\models.py 34 0 0 0 100% + src\wish\tests.py 20 0 0 0 100% + ------------------------------------------------------------------ + TOTAL 87 0 4 1 99% + +********************* +Quelques liens utiles +********************* * `Django factory boy `_ \ No newline at end of file