models and tests

This commit is contained in:
Fred Pauchet 2016-06-03 14:16:10 +02:00
parent c01a754c31
commit 21d4278373
5 changed files with 153 additions and 33 deletions

View File

@ -15,6 +15,7 @@ Contents:
intro
specs
models
tests
mvc
forms
auth

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -6,7 +6,21 @@ Tests unitaires
Méthodologies
*************
(Copié/collé à partir de `ce lien <https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.kfyvxyb21>`_):
Pourquoi s'ennuyer à écrire des tests?
======================================
Traduit grossièrement depuis un article sur `https://medium.com <https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.kfyvxyb21>`_:
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 dont 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 <https://github.com/rbarrois/django-factory_boy/tree/v1.0.0>`_