gwift-book/chapters/new-project.tex

742 lines
36 KiB
TeX
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\chapter{Démarrer un nouveau projet}
Django fonctionne sur un
\href{https://docs.djangoproject.com/en/dev/internals/release-process/}{roulement de trois versions mineures pour une version majeure}, clôturé par une version LTS (\emph{Long Term Support}).
\section{Gestion des dépendances}
Comme nous en avons déjà discuté, Poetry est la solution que nous avons choisie pour la gestion de nos dépendances.
Pour installer une nouvelle librairie, vous pouvez simplement passer par la commande \texttt{pip\ install\ \textless{}my\_awesome\_library\textgreater{}}.
Dans le cas de Django, et après avoir activé l'environnement, nous pouvons à présent y installer Django. Comme expliqué ci-dessus, la librairie restera indépendante du reste du système, et ne polluera aucun autre projet. nous exécuterons donc la commande suivante:
\begin{verbatim}
$ source ~/.venvs/gwift-env/bin/activate # ou ~/.venvs/gwift-
env/Scrips/activate.bat pour Windows.
$ pip install django
Collecting django
Downloading Django-3.1.4
100% |################################|
Installing collected packages: django
Successfully installed django-3.1.4
\end{verbatim}
Ici, la commande \texttt{pip\ install\ django} récupère la \textbf{dernière version connue disponible dans les dépôts \url{https://pypi.org/}} (sauf si vous en avez définis d'autres. Mais c'est hors sujet).
Nous en avons déjà discuté : il est important de bien spécifier la version que vous souhaitez utiliser, sans quoi vous risquez de rencontrer des effets de bord.
L'installation de Django a ajouté un nouvel exécutable: \texttt{django-admin}, que l'on peut utiliser pour créer notre nouvel espace de travail.
Par la suite, nous utiliserons \texttt{manage.py}, qui constitue un \textbf{wrapper} autour de \texttt{django-admin}.
Pour démarrer notre projet, nous lançons \texttt{django-admin\ startproject\ gwift}:
\begin{verbatim}
$ django-admin startproject gwift
\end{verbatim}
Cette action a pour effet de créer un nouveau dossier \texttt{gwift}, dans lequel nous trouvons la structure suivante :
\begin{verbatim}
$ tree gwift
gwift
-- gwift
----- asgi.py
----- __init__.py
----- settings.py
----- urls.py
----- wsgi.py
-- manage.py
\end{verbatim}
C'est dans ce répertoire que vont vivre tous les fichiers liés au projet.
Le but est de faire en sorte que toutes les opérations (maintenance, déploiement, écriture, tests, \ldots\hspace{0pt}) puissent se faire à partir d'un seul point d'entrée.
L'utilité de ces fichiers est définie ci-dessous:
\begin{itemize}
\item
\texttt{settings.py} contient tous les paramètres globaux à notre projet.
\item
\texttt{urls.py} contient les variables de routes, les adresses utilisées et les fonctions vers lesquelles elles pointent.
\item
\texttt{manage.py}, pour toutes les commandes de gestion.
\item
\texttt{asgi.py} contient la définition de l'interface \href{https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface}{ASGI}, le protocole pour la passerelle asynchrone entre votre application et le serveur Web.
\item
\texttt{wsgi.py} contient la définition de l'interface \href{https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface}{WSGI}, qui permettra à votre serveur Web (Nginx, Apache, \ldots\hspace{0pt}) de faire un pont vers votre projet.
\end{itemize}
Indiquer qu'il est possible d'avoir plusieurs structures de dossiers et qu'il n'y a pas de "magie" derrière toutes ces commandes.
La seule condition est que les chemins référencés soient cohérents par rapport à la structure sous-jacente.
Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel nous stockerons les dépendances et un fichier README:
TODO
Comme nous venons d'ajouter une dépendance à notre projet, profitons-en pour créer un fichier reprenant tous les dépendances de notre projet.
Celles-ci sont normalement placées dans un fichier \texttt{requirements.txt}.
Dans un premier temps, ce fichier peut être placé directement à la racine du projet, mais on préférera rapidement le déplacer dans un sous-répertoire spécifique (\texttt{requirements}), afin de grouper les dépendances en fonction de leur environnement de destination:
\begin{itemize}
\item
\texttt{base.txt}
\item
\texttt{dev.txt}
\item
\texttt{production.txt}
\end{itemize}
Au début de chaque fichier, il suffit d'ajouter la ligne \texttt{-r\ base.txt}, puis de lancer l'installation grâce à un \texttt{pip\ install\ -r\ \textless{}nom\ du\ fichier\textgreater{}}.
De cette manière, il est tout à fait acceptable de n'installer \texttt{flake8} et \texttt{django-debug-toolbar} qu'en développement par exemple.
Dans l'immédiat, nous allons ajouter \texttt{django} dans une version égale à la version 3.2 dans le fichier \texttt{requirements/base.txt}.
\begin{verbatim}
$ echo 'django==3.2' > requirements/base.txt
$ echo '-r base.txt' > requirements/prod.txt
$ echo '-r base.txt' > requirements/dev.txt
\end{verbatim}
Une bonne pratique consiste à également placer un fichier \texttt{requirements.txt} à la racine du projet, et dans lequel nous retrouverons le contenu \texttt{-r requirements/production.txt} (notamment pour Heroku).
Prenez directement l'habitude de spécifier la version ou les versions compatibles: les librairies que vous utilisez comme dépendances évoluent, de la même manière que vos projets.
Pour être sûr et certain le code que vous avez écrit continue à fonctionner, spécifiez la version de chaque librairie de dépendances.
Entre deux versions d'une même librairie, des fonctions sont cassées, certaines signatures sont modifiées, des comportements sont altérés, etc.
Il suffit de parcourirles pages de \emph{Changements incompatibles avec les anciennes versions dans Django}
\href{https://docs.djangoproject.com/fr/3.1/releases/3.0/}{(par exemple ici pour le passage de la 3.0 à la 3.1)} pour réaliser que certaines opérations ne sont pas anodines, et que sans filet de sécurité, c'est le
mur assuré.
Avec les mécanismes d'intégration continue et de tests unitaires, nous verrons plus loin comment se prémunir d'un changement inattendu.
\includegraphics{images/django-support-lts.png}
La version utilisée sera une bonne indication à prendre en considération pour nos dépendances, puisqu'en visant une version particulière, nous ne devrons pratiquement pas nous soucier (bon, un peu quand même, mais nous le verrons plus tard\ldots\hspace{0pt}) des dépendances à installer, pour peu que l'on reste sous un certain seuil.
Dans les étapes ci-dessous, nous épinglerons une version LTS afin de nous assurer une certaine sérénité d'esprit (= dont nous ne occuperons pas pendant les 3 prochaines années).
\section{Django}
Comme nous l'avons vu ci-dessus, \texttt{django-admin} permet de créer un nouveau projet.
Nous faisons ici une distinction entre un \textbf{projet} et une \textbf{application}:
\begin{itemize}
\item
\textbf{Un projet} représente l'ensemble des applications, paramètres, middlewares, dépendances, \ldots, qui font que votre code fait ce qu'il est sensé faire.
Il s'agit grosso modo d'un câblage de tous les composants entre eux.
\item
\textbf{Une application} est un contexte d'exécution (vues, comportements, pages HTML, \ldots), idéalement autonome, d'une partie du projet.
Une application est supposée avoir une portée de réutilisation, même s'il ne sera pas toujours possible de viser une généricité parfaite.
\end{itemize}
Pour \texttt{gwift}, nous aurons :
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-gwift.png}
\caption{Projet Django vs Applications}
\end{figure}
\begin{enumerate}
\item
Une première application pour la gestion des listes de souhaits et des éléments,
\item
Une deuxième application pour la gestion des utilisateurs,
\item
Voire une troisième application qui gérera les partages entre utilisateurs et listes.
\end{enumerate}
Nous voyons également que la gestion des listes de souhaits et éléments aura besoin de la gestion des utilisateurs - elle n'est pas autonome -, tandis que la gestion des utilisateurs n'a aucune autre dépendance
qu'elle-même.
Pour \texttt{khana}, nous pourrions avoir quelque chose comme ceci:
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-khana.png}
\caption{Django Project vs Applications}
\end{figure}
En rouge, vous pouvez voir quelque chose que nous avons déjà vu: la gestion des utilisateurs et la possibilité qu'ils auront de communiquer entre eux.
Ceci pourrait être commun aux deux projets.
Nous pouvons clairement visualiser le principe de \textbf{contexte} pour une application: celle-ci viendra avec son modèle, ses tests, ses vues et son paramétrage et pourrait ainsi être réutilisée dans un autre projet.
C'est en ça que consistent les \href{https://www.djangopackages.com/}{paquets Django} déjà disponibles:
ce sont "\emph{simplement}" de petites applications empaquetées et pouvant être réutilisées dans différents contextes (eg. \href{https://github.com/tomchristie/django-rest-framework}{Django-Rest-Framework}, \href{https://github.com/django-debug-toolbar/django-debug-toolbar}{Django-Debug-Toolbar}, \ldots
Le projet s'occupe principalement d'appliquer une couche de glue entre différentes applications.
Découper proprement un projet en plusieurs applications totalement autonomes est illusoire.
Une bonne pratique consiste à rester pragmatique et à partir avec \textbf{une seule} application, et la découper lorsque vous jugerez qu'elle grossit trop ou trop rapidement \cite[Rule \#5 : don't split files by default]{django_for_startup_founders} : découper trop rapidement et sans raison valable une application en plein de petits fichiers va gâcher énormément de temps de développement, sans apporter de réels bénéfices.
D'autre part, une (autre) bonne pratique consiste à aussi \textbf{limiter à cinq} le nombre de modèles différents dans chaque application.
Tant que ce seuil ne sera pas atteint, laissez ce principe de côté.
\subsection{manage.py}
Le fichier \texttt{manage.py} que vous trouvez à la racine de votre projet est un \textbf{wrapper} sur les commandes \texttt{django-admin}.
A partir de maintenant, nous n'utiliserons plus que celui-là pour tout ce qui touchera à la gestion de notre projet :
\begin{itemize}
\item
\texttt{manage.py\ check} pour vérifier (en surface\ldots\hspace{0pt})
que votre projet ne rencontre aucune erreur évidente
\item
\texttt{manage.py\ check\ -\/-deploy}, pour vérifier (en surface
aussi) que l'application est prête pour un déploiement
\item
\texttt{manage.py\ runserver} pour lancer un serveur de développement
\item
\texttt{manage.py\ test} pour découvrir les tests unitaires
disponibles et les lancer.
\end{itemize}
La liste complète peut être affichée avec \texttt{manage.py\ help}. Vous
remarquerez que ces commandes sont groupées selon différentes
catégories:
\begin{itemize}
\item
\textbf{auth}: création d'un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant.
\item
\textbf{django}: vérifier la \textbf{conformité} du projet, lancer un \textbf{shell}, \textbf{dumper} les données de la base, effectuer une migration du schéma, \ldots
Ce sont des commandes d'administration générale.
\item
\textbf{sessions}: suppressions des sessions en cours
\item
\textbf{staticfiles}: gestion des fichiers statiques et lancement du serveur de développement.
\end{itemize}
Chaque section correspond à une application.
En analysant le code, ces applications peuvent être trouvées sous \texttt{django.contrib}.
Nous verrons plus tard comment ajouter de nouvelles commandes.
Si nous démarrons la commande \texttt{python\ manage.py\ runserver},
nous verrons la sortie console suivante:
\begin{verbatim}
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
[...]
December 15, 2020 - 20:45:07
Django version 3.1.4, using settings 'gwift.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
\end{verbatim}
Si nous nous rendons sur la page \url{http://127.0.0.1:8000} (ou \url{http://localhost:8000}) comme le propose si gentiment notre (nouveau) meilleur ami, nous verrons ceci:
\begin{figure}
\centering
\includegraphics{images/django/manage-runserver.png}
\caption{python manage.py runserver (Non, ce n'est pas Challenger)}
\end{figure}
Nous avons mis un morceau de la sortie console entre crochet \texttt{{[}\ldots{}\hspace{0pt}{]}} ci-dessus, car elle concerne les migrations.
Si vous avez suivi les étapes jusqu'ici, vous avez également dû voir un message type \texttt{You\ have\ 18\ unapplied\ migration(s).\ {[}\ldots{}\hspace{0pt}{]}\ Run\ \textquotesingle{}python\ manage.py\ migrate\textquotesingle{}\ to\ apply\ them.}
Cela concerne les migrations, et c'est un point que nous verrons un peu plus tard.
\section{Minimal Viable Application}
Maintenant que nous avons a vu à quoi servait \texttt{manage.py}, nous pouvons créer notre nouvelle application grâce à la commande \texttt{manage.py\ startapp\ \textless{}label\textgreater{}}.
Notre première application servira à structurer des listes de souhaits, les éléments qui les composent et les pourcentages de participation que chaque utilisateur aura souhaité offrir.
De manière générale, essayez de trouver un nom éloquent, court et qui résume bien ce que fait l'application. Pour nous, ce sera donc \texttt{wish}.
\begin{verbatim}
$ python manage.py startapp wish
\end{verbatim}
Résultat? Django nous a créé un répertoire \texttt{wish}, dans lequel nous trouvons les fichiers et dossiers suivants:
\begin{itemize}
\item
\texttt{wish/\_\_init\_\_.py} pour que notre répertoire \texttt{wish} soit converti en package Python.
\item
\texttt{wish/admin.py} servira à structurer l'administration de notre application.
Chaque information peut être gérée facilement au travers d'une interface générée à la volée par le framework.
Nous y reviendrons par la suite.
\item
\texttt{wish/apps.py} qui contient la configuration de l'application et qui permet notamment de fixer un nom ou un libellé \url{https://docs.djangoproject.com/en/stable/ref/applications/}
\item
\texttt{wish/migrations/} est le dossier dans lequel seront stockées toutes les différentes migrations de notre application (= toutes les modifications que nous apporterons aux données que nous souhaiterons manipuler)
\item
\texttt{wish/models.py} représentera et structurera nos données. Ce modèle est intimement lié aux migrations.
\item
\texttt{wish/tests.py} pour les tests unitaires.
\end{itemize}
Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire \texttt{wish} dans votre répertoire \texttt{gwift} existant.
C'est \emph{une} forme de convention.
La structure de vos répertoires devient celle-ci:
TODO
Notre application a bien été créée, et nous l'avons déplacée dans le répertoire \texttt{gwift} ! \footnote{Il manque quelques fichiers utiles, qui seront décrits par la suite, pour qu'une application soit réellement autonome: templates, \texttt{urls.py}, managers, services, \ldots}
\section{Fonctionnement général}
Le métier de programmeur est devenu de plus en plus complexe.
Il y a 20 ans, nous pouvions nous contenter d'une simple page PHP dans laquelle nous mixions l'ensemble des actions à réaliser : requêtes en bases de données, construction de la page, \ldots
La recherche d'une solution à un problème n'était pas spécialement plus complexe - dans la mesure où le rendu des enregistrements en direct n'était finalement qu'une forme un chouia plus évoluée du \texttt{print()} ou des \texttt{System.out.println()} - mais c'était l'évolutivité des applications qui en prenait un coup: une grosse partie des tâches étaient dupliquées entre les différentes pages, et l'ajout d'une nouvelle fonctionnalité était relativement ardue.
Django (et d'autres frameworks) résolvent ce problème en se basant ouvertement sur le principe de \texttt{Dont\ repeat\ yourself} \footnote{DRY}.
Chaque morceau de code ne doit apparaitre qu'une seule fois, afin de limiter au maximum la redite (et donc, l'application d'un même correctif à différents endroits).
Le chemin parcouru par une requête est expliqué en (petits) détails ci-dessous.
\textbf{Un utilisateur ou un visiteur souhaite accéder à une URL hébergée et servie par notre application}.
Ici, nous prenons l'exemple de l'URL fictive \texttt{https://gwift/wishes/91827}.
Lorsque cette URL "arrive" dans notre application, son point d'entrée se trouvera au niveau des fichiers \texttt{asgi.py} ou \texttt{wsgi.py}.
Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chemin interne qu'elle va parcourir.
\begin{figure}
\centering
\includegraphics{images/diagrams/django-how-it-works.png}
\caption{How it works}
\end{figure}
\textbf{Etape 0} - La première étape consiste à vérifier que cette URL répond à un schéma que nous avons défini dans le fichier \texttt{gwift/urls.py}.
\begin{minted}{Python}
from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
urlpatterns = [
path('admin/', admin.site.urls),
path("wishes/<int:wish_id>", wish_details),
]
\end{minted}
\textbf{Etape 1} - Si ce n'est pas le cas, l'application n'ira pas plus loin et retournera une erreur à l'utilisateur.
\textbf{Etape 2} - Django va parcourir l'ensemble des \emph{patterns} présents dans le fichier \texttt{urls.py} et s'arrêtera sur le premier qui correspondra à la requête qu'il a reçue.
Ce cas est relativement trivial: la requête \texttt{/wishes/91827} a une correspondance au
niveau de la ligne \texttt{path("wishes/\textless{}int:wish\_id\textgreater{}} dans l'exemple ci-dessous. Django va alors appeler la fonction \footnote{Qui ne sera pas toujours une fonction. Django s'attend à trouver un \emph{callable}, c'est-à-dire n'importe quel élément qu'il peut appeler comme une fonction.} associée à ce \emph{pattern}, c'est-à-dire \texttt{wish\_details} du module \texttt{gwift.views}.
\begin{itemize}
\item
Nous importons la fonction \texttt{wish\_details} du module
\texttt{gwift.views}
\item
Champomy et cotillons! Nous avons une correspondance avec
\texttt{wishes/details/91827}
\end{itemize}
Le module \texttt{gwift.views} qui se trouve dans le fichier \texttt{gwift/views.py} peut ressembler à ceci:
\begin{minted}{Python}
# gwift/views.py
[...]
from datetime import datetime
def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse:
context = {
"user_name": "Bond,"
"user_first_name": "James",
"generated_at": datetime.now()
}
return render(
request,
"wish_details.html",
context
)
\end{minted}
Pour résumer, cette fonction permet:
\begin{enumerate}
\item
De construire un \emph{contexte}, qui est représenté sous la forme d'un dictionnaire associant des clés à des valeurs.
Les clés sont respectivement \texttt{user\_name}, \texttt{user\_first\_name} et \texttt{now}, tandis que leurs valeurs respectives sont \texttt{Bond}, \texttt{James} et le \texttt{moment\ présent} \footnote{Non, pas celui d'Eckhart Tolle}.
\item
Nous passons ensuite ce dictionnaire à un canevas, \texttt{wish\_details.html}, que l'on trouve normalement dans le répertoire \texttt{templates} de notre projet, ou dans le répertoire \texttt{templates} propre à notre application.
\item
L'application du contexte sur le canevas au travers de la fonction \texttt{render} nous donne un résultat formaté.
\end{enumerate}
\begin{minted}{html}
<!-- fichier wish_details.html -->
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<h1>Hi!</h1>
<p>My name is {{ user_name }}. {{ user_first_name }} {{ user_name }}.</p>
<p>This page was generated at {{ generated_at }}</p>
</body>
</html>
\end{minted}
Après application de notre contexte sur ce template, nous obtiendrons ce document, qui sera renvoyé au navigateur de l'utilisateur qui aura fait la requête initiale:
\begin{minted}{html}
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<h1>Hi!</h1>
<p>My name is Bond. James Bond.</p>
<p>This page was generated at 2027-03-19 19:47:38</p>
</body>
</html>
\end{minted}
\begin{figure}
\centering
\includegraphics{images/django/django-first-template.png}
\caption{Résultat}
\end{figure}
\section{Configuration globale}
\subsection{Structure finale}
En repartant de la structure initiale décrite au chapitre précédent, nous arrivons à ceci.
TODO : passer à poetry
\subsection{Cookie-cutter}
Pfiou! Ca en fait des commandes et du boulot pour "juste" démarrer un nouveau projet, non? Sachant qu'en plus, nous avons dû modifier des fichiers, déplacer des dossiers, ajouter des dépendances, configurer une
base de données, \ldots
Bonne nouvelle! Il existe des générateurs, permettant de démarrer rapidement un nouveau projet sans (trop) se prendre la tête.
Le plus connu (et le plus personnalisable) est \href{https://cookiecutter.readthedocs.io/}{Cookie-Cutter}, qui se base sur des canevas \emph{type \href{https://pypi.org/project/Jinja2/}{Jinja2}}, pour créer une
arborescence de dossiers et fichiers conformes à votre manière de travailler.
Et si vous avez la flemme de créer votre propre canevas, vous pouvez utiliser \href{https://cookiecutter-django.readthedocs.io}{ceux qui existent déjà}.
Pour démarrer, créez un environnement virtuel (comme d'habitude):
\begin{verbatim}
python -m venv .venvs\cookie-cutter-khana
.venvs\cookie-cutter-khana\Scripts\activate.bat
(cookie-cutter-khana) $ pip install cookiecutter
Collecting cookiecutter
[...]
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 arrow-0.17.0
binaryornot-0.4.4 certifi-2020.12.5 chardet-4.0.0 click-7.1.2 cookiecutter-
1.7.2 idna-2.10 jinja2-time-0.2.0 poyo-0.5.0 python-dateutil-2.8.1 python-
slugify-4.0.1 requests-2.25.1 six-1.15.0 text-unidecode-1.3 urllib3-1.26.2
(cookie-cutter-khana) $ cookiecutter https://github.com/pydanny/cookiecutter-
django
[...]
[SUCCESS]: Project initialized, keep up the good work!
\end{verbatim}
Si vous explorez les différents fichiers, vous trouverez beaucoup de similitudes avec la configuration que nous vous proposions ci-dessus.
En fonction de votre expérience, vous serez tenté de modifier certains paramètres, pour faire correspondre ces sources avec votre utilisation ou vos habitudes.
Il est aussi possible d'utiliser l'argument \texttt{-\/-template}, suivie d'un argument reprenant le nom de votre projet (\texttt{\textless{}my\_project\textgreater{}}), lors de l'initialisation d'un projet avec la commande \texttt{startproject} de \texttt{django-admin}, afin de calquer votre arborescence sur un projet
existant.
La \href{https://docs.djangoproject.com/en/stable/ref/django-admin/\#startproject}{documentation} à ce sujet est assez complète.
\begin{verbatim}
django-admin.py startproject --template=https://[...].zip <my_project>
\end{verbatim}
\section{Tests unitaires}
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.
Chaque application est créée par défaut avec un fichier \textbf{tests.py}, qui inclut la classe \texttt{TestCase} depuis le package \texttt{django.test}:
On a deux choix ici:
\begin{enumerate}
\item Utiliser les librairies de test de Django
\item Utiliser Pytest
\end{enumerate}
\subsection{django.test}
\begin{listing}[H]
\begin{minted}{Python}
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
\end{minted}
\end{listing}
\subsection{Pytest}
\subsection{Couverture de code}
Quel que soit le framework de tests choisi (django-tests, pytest, unittest, \ldots), la couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests.
Il ne s'agit pas de vérifier que le code est bien testé, mais de vérifier quelle partie du code est testée.
Le paquet coverage se charge dévaluer le pourcentage de code couvert par les tests.
Avec pytest, il convient dutiliser le paquet pytest-cov, suivi de la commande pytest
\texttt{--cov=gwift tests/}.
Si vous préférez rester avec le cadre de tests de Django, vous pouvez passer par le paquet django-coverage-plugin.
Ajoutez-le dans le fichier requirements/base.txt, et lancez une couverture de code grâce à la commande coverage.
La configuration peut se faire dans un fichier .coveragerc que vous placerez à la racine de votre projet, et qui sera lu lors de lexécution.
\begin{verbatim}
# requirements/base.text
[...]
django_coverage_plugin
\end{verbatim}
\begin{verbatim}
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report
\end{verbatim}
Nous pouvons à présent jouer au jeu de la couverture, qui consiste à augmenter ou égaliser la couverture existante à chaque nouvelle fonctionnalité ajoutée ou bug corrigé.
De cette manière, sans arriver à une couverture de 100\%, chaque modification du code améliorera la base existante. \footnote{cf. Two Scoops of Django}.
Suivant l'outil d'intégration continue que vous utiliserez, cette évolution pourra être affichée à chaque demande de fusion, et pourra être considérée comme un indicateur de qualité.
\begin{verbatim}
$ coverage run --source "." manage.py test
$ coverage report
Name Stmts Miss Cover
---------------------------------------------
gwift\gwift\__init__.py 0 0 100%
gwift\gwift\settings.py 17 0 100%
gwift\gwift\urls.py 5 5 0%
gwift\gwift\wsgi.py 4 4 0%
gwift\manage.py 6 0 100%
gwift\wish\__init__.py 0 0 100%
gwift\wish\admin.py 1 0 100%
gwift\wish\models.py 49 16 67%
gwift\wish\tests.py 1 1 0%
gwift\wish\views.py 6 6 0%
---------------------------------------------
TOTAL 89 32 64%
----
$ coverage html
\end{verbatim}
\subsection{Recommandations sur les tests}
En résumé, il est recommandé de:
\begin{enumerate}
\item
Tester que le nommage d'une URL (son attribut \texttt{name} dans les fichiers \texttt{urls.py}) corresponde à la fonction que l'on y a définie
\item
Tester que l'URL envoie bien vers l'exécution d'une fonction (et que cette fonction est celle que l'on attend)
\end{enumerate}
\subsubsection{Tests de nommage}
\begin{minted}{python}
from django.core.urlresolvers import reverse
from django.test import TestCase
class HomeTests(TestCase):
def test_home_view_status_code(self):
url = reverse("home")
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
\end{minted}
\subsubsection{Tests d'URLs}
\begin{minted}{python}
from django.core.urlresolvers import reverse
from django.test import TestCase
from .views import home
class HomeTests(TestCase):
def test_home_view_status_code(self):
view = resolve("/")
self.assertEquals(view.func, home)
\end{minted}
\subsection{Couverture de code}
Pour l'exemple, nous allons écrire la fonction \texttt{percentage\_of\_completion} sur la classe \texttt{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:
\begin{minted}{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
\end{minted}
Lancez maintenant la couverture de code. Vous obtiendrez ceci:
\begin{verbatim}
$ 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%
\end{verbatim}
Si vous générez le rapport HTML avec la commande \texttt{coverage\ html} et que vous ouvrez le fichier
\texttt{coverage\_html\_report/src\_wish\_models\_py.html}, vous verrez que les méthodes en rouge ne sont pas testées. \textbf{A contrario}, la couverture de code atteignait \textbf{98\%} avant l'ajout de cette
nouvelle méthode.
Pour cela, on va utiliser un fichier \texttt{tests.py} dans notre application \texttt{wish}. \textbf{A priori}, ce fichier est créé automatiquement lorsque vous initialisez une nouvelle application.
\begin{minted}{python}
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)
\end{minted}
L'attribut \texttt{@property} sur la méthode \texttt{percentage\_of\_completion()} va nous permettre d'appeler directement la méthode \texttt{percentage\_of\_completion()} comme s'il s'agissait d'une propriété de la classe, au même titre que les champs \texttt{number\_of\_parts} ou \texttt{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 \textbf{cache}, que nous aborderons plus loin.
En relançant la couverture de code, on voit à présent que nous arrivons à 99\%:
\begin{verbatim}
$ 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%
\end{verbatim}
En continuant de cette manière (ie. Ecriture du code et des tests, vérification de la couverture de code), on se fixe un objectif idéal dès le début du projet. En prenant un développement en cours de route, fixez-vous comme objectif de ne jamais faire baisser la couverture de code.
A noter que tester le modèle en lui-même (ses attributs ou champs) ou des composants internes à Django n'a pas de sens: cela reviendrait à mettre en doute son fonctionnement interne.
Selon le principe du SRP \ref{SRP}, c'est le framework lui-même qui doit en assurer la maintenance et le bon fonctionnement.
\section{Licence}
Choisissez une licence.
Si votre projet n'en a pas, vous pourriez être tenu responsable de manquements ou de bugs collatéraux.
En cas de désastre médical ou financier, ce simple fichier peut faire toute la différence.
\textit{React, for example, has an additional clause that could potentially cause patent claim conflicts with React users} \cite[p. 47]{roads_and_bridges}.
Cette issue a été adressée en 2017 \footnote{\url{hhttps://github.com/facebook/react/issues/7293}}.
Un autre exemple concerne StackOverflow, qui utilisait une licence Creative Commons de type CC-BY-SA pour chaque contenu posté sur sa plateforme.
Cette licence est cependante limitante, dans la mesure où elle obligeait que chaque utilisateur cite l'origine du code utilisé.
Ceci n'était pas vraiment connu de tous, mais si un utilisateur qui venait à opérer selon des contraintes relativement strictes (en milieu professionnel, par exemple) venait à poser une question sur la plateforme, il aurait été légalement obligé de réattribuer la réponse qu'il aurait pu utiliser.
StackOverflow est ainsi passé vers une licence MIT présentant moins de restrictions.
Trois licences \footnote{Bien qu'il en existe beaucoup} sont généralement proposées et utilisées:
\begin{enumerate}
\item
\textbf{MIT}
\item
\textbf{GPLv3}
\item
\textbf{Fair Source}, annoncée en 2015, qui propose une solution à la nécessité de proposer une licence gratuite pour une utilisation personnelle ou en petites entreprises, tout en étant payante pour une une utilisation commerciale plus large.
\footnote{\textit{Under Fair Source, code is free to view, download, execute, and modify up to a certain number of users in an organization. After that limit is reached, the organization must pay a licencing fee, determined by the published - \url{https://fair.io}}}
\item
\textbf{WTFPL}
\end{enumerate}
Mike Perham, qui maintient Sidekiq, a ainsi proposé une forme de dualité entre la mise à disposition du code source et son utilisation \cite[p. 95]{roads_and_bridges}:
\begin{quote}
\textit{Remember: Open Source is not Free Software.
The source may be viewable on GitHub but that doesn't mean anyone can use it for any purpose.
There's no reason you can't make your source code accessible but also charge to use it.
As long as you are the owner of the code, you have the right to licence it however you want.}
\textit{...[The] reality is most smaller OSS project have a single person doing 95\% of the work.
If this is true, be grateful for unpaid help but don't feel guilty about keeping 100\% of the income.}
\end{quote}
\section{Conclusions}
Comme nous l'avons vu dans la première partie, Django est un framework complet, intégrant tous les mécanismes nécessaires à la bonne évolution d'une application.
Il est possible de démarrer petit, et de suivre l'évolution des besoins en fonction de la charge estimée ou ressentie, d'ajouter un mécanisme de mise en cache, des logiciels de suivi, \ldots