gwift-book/chapters/tests.tex

173 lines
8.6 KiB
TeX
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{Tests unitaires et d'intégration}
\begin{quote}
Tests are part of the system.
You can think of tests as the outermost circle in the architecture.
Nothing within in the system depends on the tests, and the tests always depend inward on the components of the system.
-- Robert C. Martin, Clean Architecture
\end{quote}
\section{Complexité cyclomatique\index{McCabe}}
La \href{https://fr.wikipedia.org/wiki/Nombre_cyclomatique}{complexité cyclomatique} (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section.
Quand le cycle d'exécution du code rencontre une condition, cette condition peut être évalue à VRAI ou à FAUX.
L'exécution du code dispose donc de deux embranchements, correspondant chacun à un résultat de cette condition.
Le code suivant \autoref{cyclomatic-simple-code} a une complexité cyclomatique 1; il s'agit du cas le plus simple que nous pouvons implémenter: l'exécution du code rentre dans la fonction (il y a un seul embranchement), et aucun bloc conditionnel n'est présent sur son chemin.
La complexité reste de 1.
\begin{listing}[!hbpt]
\begin{minted}{Python}
from datetime import date
def print_current_date():
print(date.today())
\end{minted}
\caption{Une version ultra-simple de l'affichage du jour de la semaine}
\label{cyclomatic-simple-code}
\end{listing}
Si nous complexifions cette fonction en vérifiant (par exemple) le jour de la semaine, nous aurons notre embranchement initial (l'impression à l'écran de la date du jour), mais également un second embranchement qui vérifiera si cette date correspond à un lundi:
\begin{listing}[!h]
\begin{minted}{Python}
from datetime import date
def print_current_date_if_monday():
if date.today().weekday() == 0:
print("Aujourd'hui, c'est lundi!")
print(date.today())
\end{minted}
\caption{Ajout d'une fonctionnalité essentielle et totalement indispensable}
\end{listing}
La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1.
Si nous rencontrons une condition, elle passera à 2, etc.
Cette complexité est liée à deux concepts:
\begin{itemize}
\item \textbf{La lisibilité du code}: au plus la complexité cyclomatique sera élevée, au plus le code sera compliqué à comprendre en première instance. Il sera composé de plusieurs conditions, éventuellement imbriquées, il débordera probablement de la hauteur que votre écran sera capable d'afficher
\item \textbf{Les tests unitaires}: pour nous assurer d'une couverture de code correcte, il sera nécessaire de couvrir tous les embranchements présentés. Connaître la complexité permet de savoir combien de tests devront être écrits pour assurer une couverture complète de tous les cas pouvant se présenter.
\end{itemize}
\section{Lisibilité du code}
Il est important de noter que refactoriser un bloc, par exemple en extrayant une méthode, n'améliore pas la complexité cyclomatique globale de l'application.
L'amélioration que nous visons ici est une amélioration \textbf{locale}, qui facilite la lecture d'un bloc spécifique, et pas d'un programme complet.
"Améliorons" notre code ci-dessous, pour lui ajouter la possibilité de gérer les autres jours de la semaine:
\begin{listing}[H]
\begin{minted}{Python}
from datetime import date
def print_current_date():
if date.today().weekday() == 0:
print("Lundi")
elif date.today().weekday() == 1:
print("Mardi")
elif date.today().weekday() == 2:
print("Mercredi")
elif date.today().weekday() == 3:
print("Jeudi")
elif date.today().weekday() == 4:
print("Vendredi")
elif date.today().weekday() == 5:
print("Samedi")
elif date.today().weekday() == 6:
print("Dimanche")
print(date.today())
\end{minted}
\caption{Un code un peu nul avec une complexité cyclomatique qui l'est tout autant}
\label{Impression du jour de la semaine, version naïve}
\end{listing}
La complexité de ce code est évaluée à 8, même si la complexité effective ne sera que de 7.
Extraire une méthode à partir de ce bloc pourra réduire la complexité de la fonction \mintinline{python}{print_current_date} n'améliorera rien et ne fera que déplacer le problème.
Une solution serait de passer par un dictionnaire, de façon à ramener la complexité à 1:
\begin{listing}[H]
\begin{minted}{python}
from datetime import date
def print_current_date():
DAYS_OF_WEEK = {
0: "Lundi",
1: "Mardi",
2: "Mercredi",
3: "Jeudi",
4: "Vendredi",
5: "Samedi",
6: "Dimanche"
}
print(DAYS_OF_WEEK.get(date.today().weekday()))
print(date.today())
\end{minted}
\caption{La même version, avec une complexité réduite à 1}
\end{listing}
\section{Types de tests}
De manière générale, si nous nous rendons compte que les tests sont trop compliqués à écrire ou nous coûtent trop de temps, cest sans doute que larchitecture de la solution nest pas adaptée et que les composants sont couplés les uns aux autres.
Dans ces cas, il sera nécessaire de refactoriser le code, afin que chaque module puisse être testé
indépendamment des autres. \cite{clean_code}
Le plus important est de toujours corréler les phases de tests indépendantes du reste du travail (de développement, ici), en lautomatisant au plus près de sa source de création:
\begin{quote}
Martin Fowler observes that, in general, "a ten minute build [and test process] is perfectly within reason...
[We first] do the compilation and run tests that are more localized unit tests with the database completely stubbed out. Such tests can run very fast, keeping within the ten minutes guideline.
However any bugs that involve larger scale intercations, particularly those involving the real database, wont be found.
The second stage build runs a different suite of tests [acceptance tests] that do hit the real database and involve more end-to-end behavior.
This suite may take a couple of hours to run.
-- Robert C. Martin, Clean Architecture
\end{quote}
\subsection{Tests unitaires}
\begin{quote}
The aim of a unit test is to show that a single part of the application
does what programmer intends it to.
\end{quote}
Les tests unitaires ciblent typiquement une seule fonction, classe ou méthode, de manière isolée, en fournissant au développeur lassurance que son code réalise ce quil en attend.
Pour plusieurs raisons (et notamment en raison de performances), les tests unitaires utilisent souvent des données stubbées - pour éviter dappeler le "vrai" service
Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc.
Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil.
Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants.
Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante.
Il existe plusieurs types de tests (intégration, comportement, ...)
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux.
C'est ici qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré, le but du jeu consistant simplement à augmenter ou égaler le pourcentage de couverture de code existant avant chaque modification.
Gitlab permet de visualiser cette information de manière très propre, en l'affichant au niveau de chaque proposition d'intégration.
La couverture de code est une analyse qui donne un pourcentage lié à la
quantité de code couvert par les tests. Attention qu'il ne s'agit pas de
vérifier que le code est \textbf{bien} testé, mais juste de vérifier
\textbf{quelle partie} du code est testée. Le paquet \texttt{coverage}
se charge d'évaluer le pourcentage de code couvert par les tests.
\subsection{Tests d'acceptance}
\begin{quote}
The objective of acceptance tests is to prove that our application does
what the customer meant it to.
\end{quote}
Les tests dacceptance vérifient que lapplication fonctionne comme convenu, mais à un
plus haut niveau (fonctionnement correct dune API, validation dune chaîne dactions
effectuées par un humain, ...).
\subsection{Tests d'intégration}
Les tests dintégration vérifient que lapplication coopère correctement avec les systèmes
périphériques