220 lines
11 KiB
TeX
220 lines
11 KiB
TeX
\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}
|
||
|
||
Your tests are your first and best line of defense against software defects.
|
||
Your tests are more important than linting \& static analysis (which can only find a subclass of errors, not problems with your actual program logic).
|
||
Tests are as important as the implementation itself (all that matters is that the code meets the requirement -- how it's implemented doesn't matter at all unless it's implemented poorly).
|
||
|
||
Unit tests combine many features that make them your secret weapon to application success:
|
||
|
||
\begin{enumerate}
|
||
\item Design aid: Writing tests first gives you a clearer perspective on the ideal API design.
|
||
\item Feature documentation (for developers): Test descriptions enshrine in code every implemented feature requirement.
|
||
\item
|
||
Test your developer understanding: Does the developer understand the problem enough to articulate in code all critical component requirements?
|
||
\item
|
||
Quality Assurance: Manual QA is error prone.
|
||
In my experience, it's impossible for a developer to remember all features that need testing after making a change to refactor, add new features, or remove features.
|
||
\item
|
||
Continuous Delivery Aid: Automated QA affords the opportunity to automatically prevent broken builds from being deployed to production.
|
||
\end{enumerate}
|
||
|
||
Unit tests don't need to be twisted or manipulated to serve all of those broad-ranging goals.
|
||
Rather, it is in the essential nature of a unit test to satisfy all of those needs.
|
||
These benefits are all side-effects of a well-written test suite with good coverage.
|
||
|
||
\begin{enumerate}
|
||
\item What component aspect are you testing?
|
||
\item What should the feature do? What specific behavior requirement are you testing?
|
||
\end{enumerate}
|
||
|
||
Traduit grossièrement depuis un article sur \url{https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d\#.kfyvxyb21\%3E\%60_} :
|
||
|
||
% TODO : Finir le verbatim ci-dessous : "ils sont\ldots. ??????"
|
||
\begin{verbatim}
|
||
Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont
|
||
\end{verbatim}
|
||
|
||
% TODO : Pourquoi il finit par ":" le verbatim ci-dessous ?
|
||
\begin{verbatim}
|
||
Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi:
|
||
\end{verbatim}
|
||
|
||
\begin{enumerate}
|
||
\item
|
||
Aide au design: écrire des tests avant d'écrire le code vous donnera une meilleure perspective sur le design à appliquer aux API.
|
||
\item Documentation (pour les développeurs): chaque description d'un test
|
||
\item Tester votre compréhension en tant que développeur:
|
||
\item Assurance qualité: des tests, 5.
|
||
\end{enumerate}
|
||
|
||
|
||
\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, c’est sans doute que l’architecture de la solution n’est 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 l’automatisant 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\ldots
|
||
[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, won’t 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 l’assurance que son code réalise ce qu’il en attend.
|
||
Pour plusieurs raisons (et notamment en raison de performances), les tests unitaires utilisent souvent des données stubbées - pour éviter d’appeler 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, \ldots)
|
||
|
||
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 d’acceptance vérifient que l’application fonctionne comme convenu, mais à un plus haut niveau (fonctionnement correct d’une API, validation d’une chaîne d’actions effectuées par un humain, \ldots).
|
||
|
||
\subsection{Tests d'intégration}
|
||
|
||
Les tests d’intégration vérifient que l’application coopère correctement avec les systèmes périphériques.
|