Travaux de recherche
6 octobre 2025

Mémoire de Mastère II : TDD et CI, quels sont leurs impact sur l'efficacité et la fiabilité des projets ?

Image de couverture - Mémoire de Mastère II : TDD et CI, quels sont leurs impact sur l'efficacité et la fiabilité des projets ?

De nos jours, le Test Driven Development et l’Intégration Continue paraissent comme étant les pratiques à absolument adopter. Grâce à leur popularité grandissante depuis ces 10 dernières années, elles sont aujourd’hui devenus incontournables et sont désormais indissociables du développement modernes. Mais quel est leur impact réel sur l’efficacité des développeurs et la qualité des projets ?

C’est la question à laquelle j’ai décidé de répondre dans mon mémoire de fin de Mastère 2, “Développeur Manager Full-Stack” à l’Efrei Paris (titre RNCP de niveau 7), que je vous propose de découvrir dans cet article. :D

Sachez que le document présenté n’a pas été modifié depuis sa soumission pour correction en juin 2025.

Remerciements

Avant de commencer ce mémoire, j’aimerai remercier mon maître d’apprentissage Steve CHAMAILLARD, ainsi que Nicolas SCHUBHAN qui l’a précédé jusqu’en juillet 2024. Je tiens également à remercier l’entreprise Kang chez qui j’effectue mon alternance depuis avril 2022, ainsi que mes collègues, avec lesquels j’ai pu passer de nombreux bons moments.

Je remercie aussi l’Efrei, plus particulièrement Léa DELACROIX et Imane HABBOUB KAJIOU, qui nous a encadré dans l’écriture de ce mémoire.

Introduction

De nos jours, le développement logiciel est devenu un pilier fondamental de l’innovation technologique. Pour en témoigner, observons simplement la révolution des intelligences artificielles et leur popularisation par OpenAI courant 2020 pour se rendre compte de l’importance de ce secteur. Les entreprises sont constamment en quête d’amélioration de leur produit, et essaient parallèlement de réduire au maximum leurs temps de mise sur le marché. À ce paradoxe apparent, vient s’ajouter l’exigence et les attentes des utilisateurs ainsi qu’une concurrence pouvant être féroce. C’est face à ces enjeux majeurs formant de réels défis que doivent composer les développeurs. Développer plus vite tout en garantissant la fiabilité du code est devenu un impératif.

Dans ce contexte, les entreprises ont dû repenser leurs méthodologies de développement. L’ère des cycles de développement longs, suivis de phases de tests tardives et laborieuses se doit d’être révolue. C’est ainsi qu’ont émergé deux pratiques complémentaires particulièrement prometteuses : le Test-Driven Development (TDD) et l’Intégration Continue (CI).

Le Test-Driven Development, présenté par Kent Beck dans le début des années 2000 au travers de son livre “Test-Driven Development: By Example”, mais formalisé un peu plus tôt dans le cadre de l’Extreme Programming, est une approche qui place les tests au cœur du processus de développement. Plutôt que de traditionnellement développer l’application puis ensuite réaliser des séries de tests, le TDD fait l’exercice inverse. Le développeur réalise une série de tests pour chaque fonctionnalité de son application, créant ainsi une sorte de spécification à suivre, puis se met à la développer. De cette manière, le code est non seulement garanti d’obtenir une très bonne couverture en tests, mais il est également garanti d’être bien plus facilement maintenable que s’il avait été créé avec les méthodes traditionnelles.

Parallèlement, l’Intégration Continue, popularisée par Martin Fowler, prône l’automatisation systémique des processus de construction. Entendez par là, l’exécution automatisée des tests, la compilation des projets (si nécessaire), ou encore toutes autres tâches répétitives devant être exécutées avant la mise en production d’un projet, et pouvant aider à la détection de problèmes d’intégration. Ainsi, le phénomène d’“enfer d’intégration” (integration hell) qui caractérisait les cycles de développement traditionnels, se trouve être fortement réduit.

Aujourd’hui, la combinaison de ces deux méthodologies de développement se trouve être de plus en plus populaire. Avec la multiplication des outils, des frameworks et des services en lignes dédiés aux développeurs, ce qui était autrefois un idéal difficile à accomplir paraît aujourd’hui accessible. Toutefois, en raison de la complexité de ces méthodologies et en raison du temps d’adaptation requis pour certains développeurs avant de les maîtriser, il est assez aisé d’y trouver des réfractaires. C’est pourquoi j’ai trouvé intéressant d’approfondir ce sujet et de porter mon étude autour de la problématique suivante : Dans quelle mesure l’implémentation du Test-Driven Development (TDD), couplée à des pratiques d’Intégration Continue, permet-elle d’améliorer l’efficacité des développeurs en réduisant le temps de développement, tout en augmentant la fiabilité et la maintenabilité des projets logiciels ?

Cette problématique se trouve être tout à fait pertinente dans le contexte actuel, où la dette technique accumulée par certaines entreprises dans de nombreux projets pèse lourd sur leur capacité d’innovation. Les projets dits “legacy”, souvent dépourvus de tests automatisés car développés et conceptualisés à une époque où les pratiques modernes de DevOps n’étaient pas encore populaires, constituent un terrain d’étude intéressant.

Pour répondre à notre problématique, ce mémoire adopte une approche théorique. Le sujet étant relativement complexe et s’étendant à un certain nombre de concepts informatiques, notre plan sera découpé en plusieurs sous-parties. Ainsi, dans un premier temps, nous verrons en détail les fondements conceptuels du TDD et de la CI, leur historique, leur principe et leurs avantages. Puis, dans un second temps, nous analyserons leur impact sur le temps de développement et la fiabilité des projets.

Chapitre 1 : Fondements théoriques

1.1. Présentation du TDD

Comme nous l’avons vu dans l’introduction le Test-Driven Development (TDD), ou développement piloté par les tests en français, est une méthodologie de développement qui consiste à placer l’écriture des tests avant l’écriture du code fonctionnel. C’est en cela qu’elle constitue un renversement fondamental du paradigme traditionnel de développement, modifiant profondément l’approche de conception et d’implémentation du projet à créer.

1.1.1. Historique et origines

L’idée d’écrire les tests avant le code fonctionnel n’est pas nouvelle. En effet, IBM avait déjà développé certaines techniques pouvant s’apparenter à du TDD dans les années 1960, notamment lors de réalisation de projets pour la Nasa. Pour illustrer mon propos, je pense au projet “Mercury” qui se reposait sur de petites itérations de code, qui étaient ensuite relues, examinées et testées par les ingénieurs d’IBM. Cependant, c’est bien Kent Beck qui a popularisé la pratique en l’introduisant comme un des piliers de l’Extreme Programming (XP) à la fin des années 1990 (1999).

Beck reviendra un peu plus tard plus en détail sur cette approche au travers de son ouvrage “Test-Driven Development: By Example” (2002), dans lequel il expliquera comment le processus d’écriture des tests avant le code fonctionnel produit non seulement et de manière assez logique, un code mieux testé, mais également une conception plus élégante et plus modulaire.

Il est important de noter que cette méthodologie s’inscrivait parfaitement dans le mouvement naissant du développement agile, alors popularisé en 2001 par le “Manifeste pour le développement Agile de logiciels”, prônant l’adaptabilité, la qualité et la livraison incrémentale des projets.

Malgré une timide adoption durant les premières années, le TDD a gagné en popularité suite à son adoption par de grandes entreprises de la tech telles que Google, Microsoft, Amazon ou encore plus récemment, Netflix. Son intégration dans les derniers programmes universitaires (je pense notamment au tout récent BUT informatique) témoigne de sa reconnaissance comme pratique à suivre pour la réalisation de nouveaux projets.

1.1.2. Principes fondamentaux

Le Test-Driven Development repose sur plusieurs principes fondamentaux. Le premier est l’écriture préalable des tests. En effet, comme précédemment expliqué, le TDD s’oppose à l’approche traditionnelle en plaçant l’écriture des tests avant le code fonctionnel, et non après (s’ils sont créés).

Ensuite, le TDD impose une granularité fine. Dans notre cas, la granularité est la complexité, la taille, l’importance d’une modification apportée au code d’un programme. Lorsque l’on parle de granularité fine dans le cadre du TDD, on parle de petites modifications apportées à une base de code. L’idée derrière ce principe est de pouvoir garder le contrôle sur la progression du projet logiciel au fil du temps, en minimisant les écarts de conception potentiels résultant d’un trop grand nombre de modifications.

L’évolution organique du design est également un des principes du TDD. Plutôt que de concevoir l’ensemble de l’architecture du programme à l’avance, on la définit progressivement au cours du développement au travers des besoins exprimés par les tests. Bien entendu, on ne parle pas ici de besoins exprimés par le cahier des charges, mais bien de l’architecture interne du programme qui permettra de répondre à la demande du client (s’il y a).

Nous pouvons également citer la validation continue qui évite les problèmes de régression du code, que nous verrons plus en détail un peu plus tard, ainsi que la documentation vivante du code formée par les tests. Meilleurs que des exemples de code fournis par une documentation traditionnelle, les tests sont exécutables et modifiables à souhait. Il devient alors plus facile de reprendre un projet existant et d’en comprendre son fonctionnement.

Mais le TDD ne repose pas uniquement sur des principes, il repose également sur un cycle de développement aux étapes bien distinctes.

1.1.3. Le cycle Red-Green-Refactor

Dans le cycle du Test-Driven Development, les développeurs suivent 3 étapes clés auxquelles on a associé une couleur. Rouge, vert et bleu.

  1. Phase Rouge (Red) : Le développeur écrit un test qui échoue volontairement, qui reflète une fonctionnalité qui n’est pas encore implémentée. Ici, le rouge représente donc l’échec du test.

  2. Phase Verte (Green) : Le développeur implémente la fonctionnalité afin de réussir l’exécution du test qu’il a précédemment écrit. En général, dans cette phase on ne fait pas attention à la vitesse d’exécution du code. On écrit de manière “sale”, sans prendre en compte quelconque optimisation potentielle. Notre unique but est d’implémenter la fonctionnalité et de faire en sorte qu’elle fonctionne comme prévu. La couleur verte représente donc la réussite du test.

  3. Phase de Refactoring (Refactor) : Une fois la fonctionnalité implémentée et le test exécuté avec succès, on passe à l’étape de refactorisation. Un peu comme en mathématiques lorsqu’il s’agissait de factoriser les expressions littérales, ici on va tenter de simplifier le code le plus possible. On va par exemple éviter les duplications, simplifier les opérations complexes (si c’est possible), et cela sans faire échouer le test que l’on a précédemment écrit. On a donné la couleur bleue à cette étape.

Illustration 1: Diagramme du cycle Red-Green-Refactor

L’efficacité de ce cycle repose sur sa capacité à maintenir le développeur concentré sur un objectif précis à chaque étape, évitant ainsi de le disperser sur plusieurs objectifs pouvant représenter une charge de travail conséquente et créer un effet de tunnel (illusion d’un développement interminable).

1.1.4. Avantages théoriques du TDD

Sur le plan théorique, le TDD possède de nombreux avantages qui justifient son adoption croissante :

  1. Clarification des exigences : En traduisant les exigences du cahier des charges en tests exécutables, le TDD crée une sorte de spécification technique à suivre avant même de commencer l’implémentation des fonctionnalités.

  2. Conception modulaire : De manière générale, pour tester correctement du code, ce dernier doit être découpé pour séparer ses différentes fonctions métier. On doit respecter le principe de responsabilité unique, facilitant ainsi la création d’une bonne architecture logicielle.

  3. Détection précoce des bugs : Avec le TDD, les mauvaises surprises sont relativement rares. Étant donné que chaque modification apportée au code est testée avant d’être publiée, la correction des bugs se fait rapidement sans coût supplémentaire.

  4. Confiance dans les modifications : Comme l’ensemble du projet est testé, si une modification est publiée c’est qu’elle n’a pas altéré le fonctionnement du programme. En tout cas, elle n’introduit pas de régression par rapport aux tests écrits. On peut donc lui accorder une certaine confiance.

1.1.5. Types de tests dans l’écosystème TDD

Bien que le TDD se concentre principalement sur les tests unitaires, il existe plusieurs “sous-types” de tests étant chacun spécialisés dans un domaine.

  1. Tests unitaires : Les tests unitaires vérifient le fonctionnement d’une partie de code isolée, en général une classe ou une méthode, en simulant ses dépendances si nécessaire. Par exemple, sur Laravel, on va parler de “Mocking” pour simuler d’autres classes.

  2. Tests d’intégration : Les tests d’intégration permettent de s’assurer que tous les composants du projet fonctionnent correctement. Par exemple, on peut imaginer des tests vérifiant l’accès à une base de données, ou à une API externe.

  3. Tests fonctionnels : Les tests fonctionnels ont pour but de réaliser des tests du point de vue de l’utilisateur. Par exemple, toujours sur Laravel, un test fonctionnel peut consister en la réalisation d’un appel http vers une route du projet, et de comparer par la suite la réponse de la requête par rapport aux données que nous attendons.

  4. Tests d’acceptation : Les tests d’acceptation veillent à ce que le projet réponde aux exigences métier définies par le cahier des charges et satisfait les besoins des utilisateurs.

Dans les faits, le TDD est appliqué avec plusieurs de ces types de tests, les plus fréquents étant les tests unitaires et les tests fonctionnels. D’autres types de tests comme les tests d’acceptation sont apparus avec le temps afin de compléter certains cas que ne couvraient pas les premiers tests.

Ainsi, nous avons vu que le TDD est bien plus qu’une technique de test. C’est un paradigme de développement qui influence profondément la manière dont le code est conçu, écrit et maintenu. Passons maintenant à la présentation de l’Intégration Continue, à laquelle est souvent couplé le TDD.

1.2. Concepts de l’Intégration Continue

Tout comme le TDD, l’Intégration Continue (CI) a beaucoup gagné en popularité durant ces dernières années grâce à sa capacité à créer un environnement propice au développement d’un code de haute qualité. Elle constitue l’un des piliers des pratiques DevOps modernes et représente une avancée majeure dans la manière dont les développeurs collaborent et livrent du code.

1.2.1. Définition et principes fondamentaux

L’Intégration Continue, comme son nom le laisse supposer, vise à intégrer régulièrement le travail, les modifications de chaque développeur dans un référentiel commun. Concrètement, cela signifie que les membres d’une équipe envoient leur code plusieurs fois par jour dans un dépôt partagé, et qu’à chaque fois, un ensemble de processus automatisés se déclenche pour vérifier que tout fonctionne correctement. C’est en cela que le Test-Driven Development s’allie parfaitement bien avec l’Intégration Continue.

Martin Fowler, l’une des personnes les plus influentes de ce domaine et dont il est le principal promoteur, a défini cette pratique comme “une méthode où les développeurs intègrent fréquemment leur travail, conduisant à plusieurs intégrations quotidiennes, chacune étant vérifiée par un build automatisé incluant des tests pour repérer rapidement les erreurs d’intégration” (Martin Fowler, 2006). Cette approche peut sembler simple, mais elle révolutionne la manière de collaborer sur un projet.

À la base de l’Intégration Continue, on retrouve plusieurs principes clés :

  1. Un référentiel centralisé bien maintenu : Tous les éléments nécessaires pour construire notre projet (code, assets, configurations, etc.) sont stockés dans un système de gestion de versions. De nos jours, on utilise généralement Git, système créé par Linus Torvalds, créateur du kernel Linux. Mais d’autres systèmes comme SVN existent encore dans certaines entreprises.

  2. L’automatisation du processus de build : Fini le temps où il fallait compiler manuellement ou lancer les tests à la main. Tout ce processus est désormais automatisé, ce qui garantit que les opérations sont toujours exécutées de la même façon, évitant ainsi le fameux “mais ça marche sur ma machine !”, ce à quoi on répond souvent “on ne va pas donner ta machine au client !”.

  3. Des tests systématiques après chaque modification : Chaque build inclut l’exécution de tests automatisés qui valident que notre système continue de fonctionner comme prévu après les modifications apportées.

  4. Une fréquence d’intégration élevée : Les développeurs ne gardent pas leur code pendant des jours sur leur machine. Ils l’envoient au moins quotidiennement dans le dépôt principal, et peuvent se mettre à jour par rapport à la branche principale, ce qui limite beaucoup les divergences entre les différentes branches de développement.

  5. Des environnements de test fidèles à la production : Pour être vraiment efficaces, les tests doivent s’exécuter dans des environnements qui reflètent au maximum les conditions réelles d’exploitation. C’est un point souvent négligé mais essentiel pour éviter les mauvaises surprises lors du déploiement.

  6. Une transparence totale des résultats : L’état du build et des tests est visible par toute l’équipe, favorisant ainsi la prise de conscience collective et la réactivité face aux problèmes.

  7. Le déploiement automatisé : Dans sa forme la plus avancée, l’Intégration Continue peut s’étendre jusqu’au déploiement automatique du code validé dans différents environnements. On parle alors de livraison continue (CD) ou même de déploiement continu, qui constituent les étapes suivantes dans la chaîne DevOps.

Tous ces principes visent un objectif commun : éviter ce que les développeurs appellent “l’enfer de l’intégration” (integration hell). Ce phénomène, bien connu dans les méthodologies traditionnelles, se produit lorsque plusieurs développeurs travaillent isolément pendant de longues périodes, puis tentent d’intégrer leurs modifications. Parfois, les conflits étant si nombreux, les développeurs peuvent en venir à se demander s’il ne serait pas plus simple de tout recommencer.

1.2.2. Évolution historique des pratiques CI

L’idée d’intégrer fréquemment le code n’est pas nouvelle en soi. En réalité, comme beaucoup de bonnes pratiques en informatique, elle a connu une évolution progressive au fil des décennies. Si la notion d’intégration régulière existait déjà de façon informelle, sa transformation en pratique systématique et son automatisation complète sont des développements relativement récents qui ont changé notre façon de travailler. C’est pourquoi j’ai trouvé intéressant de retracer son historique depuis ses débuts.

Dans les années 1990, certaines équipes pionnières commençaient déjà à réaliser l’importance d’intégrer quotidiennement leur code. Cependant, ces pratiques restaient essentiellement manuelles, laborieuses et sujettes à erreurs. Un exemple assez représentatif de cette période est le développement du kernel Linux. Les contributeurs devaient envoyer leurs modifications directement par mail à Linus Torvalds, qui les examinait et les intégrait manuellement. Imaginez la complexité de cette tâche quand le projet a commencé à prendre de l’ampleur. C’est d’ailleurs ce processus d’intégration fastidieux qui a plus tard amené Linus à refuser catégoriquement d’utiliser SVN (qu’il considérait comme inadapté à un développement distribué), pour finalement créer Git en 2005. Cette anecdote illustre parfaitement les défis d’intégration que rencontraient les développeurs avant l’automatisation.

Le début des années 2000 a vu naître l’Extreme Programming (XP), popularisé par Kent Beck (le même que pour le TDD). Ce concept a introduit l’Intégration Continue comme l’une de ses douze pratiques fondamentales. L’année 2001 marque un tournant décisif avec le développement du premier serveur CI open source, CruiseControl, par ThoughtWorks. Parallèlement, la création de Git par Linus Torvalds en 2005 a révolutionné la gestion de versions en facilitant les branches et fusions, créant ainsi un terreau fertile pour l’adoption généralisée des pratiques d’Intégration Continue. À ce moment-là, on commence réellement à voir émerger une standardisation de ces pratiques.

La période 2004-2010 voit l’émergence d’outils CI plus sophistiqués, rendant la pratique accessible à un plus large éventail d’organisations. Hudson, qui a été développé par la désormais défunte Sun Microsystems (et qui deviendra plus tard Jenkins suite à un différend lors du rachat de Sun par Oracle), TeamCity et Bamboo font leur apparition et commencent à se démocratiser. Ces outils ont permis à des équipes de toutes tailles d’adopter l’Intégration Continue sans nécessiter des compétences d’administration système avancées (sysAdmin).

Nous arrivons à une période plus proche de nous : 2010-2015. L’explosion du cloud computing change radicalement la donne. Parallèlement, l’arrivée de Docker et la conteneurisation révolutionnent complètement les possibilités d’environnements de test reproductibles. Fini le “ça marche sur ma machine”, désormais, les environnements d’exécution peuvent être définis avec précision et reproduits à l’identique, renforçant considérablement l’efficacité de la CI. C’est à mon sens l’une des avancées les plus significatives de cette période pour le développement logiciel en général. À titre personnel, Docker a résolu de nombreux problèmes d’installation d’environnement de développement, notamment liés à l’exécution de PHP sur Windows.

Enfin, depuis 2015, nous assistons à une intégration de plus en plus poussée de la CI dans des plateformes complètes de gestion du cycle de vie des applications. GitHub Actions, GitLab CI/CD, et Azure DevOps en sont d’excellents exemples. Parallèlement, l’émergence du concept d’Infrastructure as Code (IaC) marque la maturité de ces pratiques, permettant de traiter l’infrastructure comme on traite le code : versionnable, testable et automatisable.

Cette évolution reflète une tendance plus large vers l’automatisation et l’intégration des différentes phases du développement logiciel. On constate que les frontières traditionnelles entre développement, test et opérations s’estompent progressivement, annonçant l’ère DevOps que nous connaissons aujourd’hui. Il est assez intéressant de voir comment nous sommes passés en à peine deux décennies de processus d’intégration manuels et pénibles, à des pipelines entièrement automatisés capables de déployer du code en production en quelques minutes.

1.2.3. Outils principaux de CI/CD

Le paysage des outils d’Intégration Continue s’est beaucoup enrichi au fil des années. Aujourd’hui, nous disposons d’une multitude de solutions adaptées à différents contextes et échelles d’organisation. Certains outils sont plus adaptés aux petites équipes, d’autres aux grandes entreprises avec des besoins complexes. Parmi les solutions que je connais, voici les plus influentes :

  1. Jenkins : Véritable pionnier open source du domaine, Jenkins reste l’une des solutions les plus utilisées malgré son grand âge (on parle d’une solution qui n’a que peu changé depuis le rachat de Sun Microsystems par Oracle en 2009). Sa force réside dans sa grande flexibilité grâce à un écosystème de plus de 1500 plugins. J’ai personnellement travaillé sur un petit projet Java utilisant Jenkins, dans le cadre de la création d’un plugin pour serveur Minecraft fonctionnant grâce à l’API Bukkit.

  2. GitLab CI/CD : Ce que j’apprécie avec GitLab CI/CD, c’est son intégration directe à la plateforme GitLab. Cela offre une expérience unifiée, de la gestion du code à son déploiement. La configuration basée sur YAML est claire et permet un haut degré de configuration. Le fait que tout soit centralisé au même endroit simplifie grandement la vie des développeurs, plus besoin de jongler entre différents outils pour suivre l’état de votre code et de vos déploiements. Nous l’utilisons intensément dans notre entreprise pour gérer les pipelines de nos projets basés sur Laravel.

  3. GitHub Actions : Relativement récent dans l’écosystème (lancé en 2019), GitHub Actions s’est rapidement imposé grâce à son intégration native avec la plateforme GitHub. Ce qui le rend attrayant, c’est son large écosystème d’actions réutilisables, que l’on pourrait associer à des sortes de plugins clés en main. Il suffit de naviguer dans le marketplace pour trouver des actions prêtes à l’emploi pour presque n’importe quelle tâche. C’est un peu comme avoir une bibliothèque de composants pour votre pipeline CI/CD. Sa configuration est très simple, mais peut-être pas assez adaptée pour les grandes entreprises.

  4. Travis CI : Particulièrement populaire pour les projets open source, Travis CI offre une intégration étroite avec GitHub et une configuration remarquablement simple. Son modèle gratuit pour les projets open source a contribué à sa grande adoption dans cette communauté. Cependant, il faut noter que depuis la sortie de GitHub Actions, ce dernier lui fait une concurrence sérieuse sur ce créneau.

  5. TeamCity : Développé par JetBrains (les créateurs d’IntelliJ IDEA, PhpStorm, etc.), TeamCity offre des fonctionnalités avancées comme la détection intelligente des tests impactés par les changements.

Ces différents outils, bien que chacun ayant ses particularités, partagent des fonctionnalités communes essentielles à toute solution de CI/CD moderne :

  • Configuration des pipelines par code (Pipeline as Code) : Les pipelines sont entièrement configurables via des fichiers versionnables, comme par exemple au travers du fichier gitlab-ci.yml pour GitLab.

  • Déclenchement automatique sur certains événements : Que ce soit un simple push, une merge request, ou même un changement dans un fichier spécifique, ces outils peuvent déclencher automatiquement vos pipelines au moment souhaité.

  • Exécution parallèle des tâches : Pour optimiser les temps d’exécution, la plupart des outils modernes permettent d’exécuter plusieurs tâches en parallèle (à l’exception de GitLab dans son offre gratuite), ce qui est assez utile pour les suites de tests volumineuses.

  • Notification des résultats de build : Ces plateformes s’intègrent avec divers canaux de communication (email, Slack, Teams, etc.) pour vous tenir informé de l’état de vos builds, vous permettant de réagir rapidement en cas de problème.

  • Gestion des artefacts : Les outils modernes ne se contentent pas d’exécuter des tests, ils gèrent également les artefacts produits (binaires, packages, images Docker, etc.), les stockent et les rendent disponibles pour les étapes ultérieures du pipeline. Par exemple, il est assez commun de télécharger des plugins pour serveur Minecraft directement depuis leur pipeline Jenkins ou GitLab.

  • Intégration avec divers outils d’analyse : De l’analyse statique de code à la mesure de la couverture de test, ces plateformes s’intègrent avec une multitude d’outils tiers tels que Codecov pour vous fournir des informations sur la qualité de votre code.

Le choix d’un outil particulier dépendra de nombreux facteurs : la taille de votre équipe, vos besoins spécifiques, votre budget, ou encore l’écosystème technique dans lequel vous évoluez déjà. Mais une chose est certaine : l’adoption d’une solution CI/CD est aujourd’hui incontournable pour toute équipe de développement souhaitant livrer un projet de qualité.

Illustration 2: Architecture d'un système d'Intégration Continue

1.2.4. Rôle dans le pipeline de développement

Le rôle de l’intégration continue au sein de l’écosystème de développement moderne est central. Imaginez-la comme un pont reliant le travail des développeurs aux environnements de test et de production. L’intégration ne se limite pas à une simple vérification technique, mais peut réaliser une multitude d’actions utiles pour les développeurs.

  1. Validation technique en temps réel : Voyez la CI comme un gardien qui vérifie constamment que votre code compile correctement, que tous vos tests passent et que les standards de qualité que vous avez définis sont bien respectés. L’idée est de mettre fin aux piles de tâches en attente. Fini la colonne “fait en dev” longue de 98 tâches, plus besoin d’attendre des jours, des semaines (ou des mois) pour savoir si votre code fonctionne, vous avez un retour quasi immédiat.

  2. Détection précoce des problèmes : Un des grands avantages de la CI est sa capacité à repérer les bugs dès leur apparition. En identifiant les problèmes rapidement, vous réduisez considérablement le coût et le temps nécessaires à leur correction.

  3. Production d’artefacts prêts à l’emploi : La CI ne se contente pas de vérifier votre code, elle peut également générer des packages déployables (qu’il s’agisse de JAR, WAR, d’images Docker ou autre) qui sont versionnés et stockés pour être utilisés plus tard. L’illustration ci-dessous illustre un package Java compilé lors d’un pipeline CI.

  4. Orchestration intelligente : Dans les configurations avancées, la CI devient un véritable chef d’orchestre, coordonnant une séquence d’étapes allant de la compilation aux tests, en passant par l’analyse statique de code.

Illustration 3: Package Java compilé via un pipeline GitLab CI

Pour les entreprises et organisations les plus matures techniquement, ce pipeline évolue de façon naturelle vers la livraison continue (CD - Continuous Delivery), où chaque modification validée par les tests devient automatiquement candidate au déploiement en production. Dans sa forme la plus avancée, on atteint même le déploiement continu qui consiste à automatiser la mise en production. Cette progression marque l’aboutissement d’une démarche DevOps complète, réduisant le délai entre une modification du code et sa disponibilité pour les utilisateurs finaux à quelques minutes seulement.

1.2.5. Pratiques avancées de CI

Au-delà des fonctionnalités de base, la CI propose un grand nombre d’actions qui ont pour but d’aider les développeurs dans leurs tâches quotidiennes. Pour des raisons de légitimité, je me concentrerai ici sur celles que j’ai pu expérimenter.

La parallélisation des tests est sans doute la pratique qui a eu le plus grand impact sur mes projets au quotidien. Sur les projets Laravel notamment (encore et toujours Laravel), j’ai pu constater des améliorations de performance assez impressionnantes. Pour vous donner un exemple concret, sur l’un de mes projets personnels comportant plusieurs centaines de tests (406 pour être précis), l’exécution séquentielle via PHPUnit prenait initialement près de 12 minutes. Un vrai frein à la productivité et cela ne donne pas envie de créer davantage de tests.

Après avoir configuré la parallélisation des tests grâce à la fonctionnalité Paratests de PHPUnit, ces mêmes tests s’exécutent désormais en à peine 30 secondes. Le principe est simple mais puissant : PHPUnit crée plusieurs processus qui exécutent chacun une partie des tests simultanément, exploitant ainsi pleinement les différents cœurs de nos processeurs.

Tout en allant de pair avec la parallélisation des tests, l’analyse de couverture de code peut être intéressante pour les projets sensibles se devant d’avoir une fiabilité irréprochable. Toutefois, il est important de noter que couvrir entièrement un projet en tests représente une lourde charge de travail. Cela nécessite encore plus d’implication que la création de tests dans le cadre du TDD, chaque ligne de code devant être testée.

Il existe également d’autres pratiques, mais dont je n’ai pas ou peu eu l’occasion d’explorer en profondeur. On peut citer notamment :

  • Les tests de performance automatisés pour mesurer et prévenir les potentielles baisses de performance
  • Le branch by abstraction pour introduire des changements majeurs progressivement
  • Les feature toggles qui permettent d’activer/désactiver des fonctionnalités à la volée
  • La gestion automatisée des données de test pour garantir la reproductibilité des tests
  • Les analyses de sécurité automatiques pour détecter les vulnérabilités avant déploiement

Ainsi, l’Intégration Continue représente bien plus qu’une simple collection d’outils ou de techniques. C’est une révolution dans la manière dont les développeurs livrent du code. Lorsqu’elle est associée avec des méthodologies modernes comme le TDD, nous obtenons une sorte de synergie où chaque méthodologie se complète mutuellement, créant ainsi une sorte de gage de qualité pour le projet logiciel concerné. C’est cette complémentarité que nous allons maintenant explorer plus en détail.

1.3. Synergie entre TDD et CI

L’usage mutuel de la méthodologie TDD et de l’Intégration Continue est devenu populaire au cours de ces 10 dernières années. L’évolution des frameworks et outils utilisés par les développeurs a facilité leur approche et il est aujourd’hui assez aisé d’y trouver des projets les employant. Mais est-ce pour autant que les utiliser conjointement représente une bonne idée ? Cela ne représenterait-il pas un effort supplémentaire dont les développeurs se passeraient bien ? Dans cette section, nous allons voir plus en détail en quoi ces deux idées sont fausses, même si la réponse est bien évidemment plus nuancée.

1.3.1. Complémentarité des approches

Une très bonne métaphore pour représenter la TDD et la CI serait de les comparer à deux pièces d’un même puzzle qui s’emboîtent parfaitement. Ils abordent la qualité logicielle sous des angles différents mais complémentaires.

Pour commencer, il faut savoir que le TDD et la CI opèrent à différentes échelles : le premier (TDD) opère à l’échelle du développeur de façon individuelle, tandis que l’autre (la CI) intervient à l’échelle de l’équipe entière. Pour faire simple, c’est comme avoir à la fois une loupe pour examiner les détails et des jumelles pour voir l’ensemble du paysage. Les usages sont différents, mais leur objectif est commun.

Ensuite, la synergie proposée par la combinaison de ces deux approches permet d’obtenir des niveaux de tests complémentaires. En effet, là où le TDD se concentre principalement sur les tests unitaires, l’Intégration Continue, elle, peut orchestrer toute une série de validations supplémentaires : tests d’intégration, fonctionnels, end to end, de performance, de sécurité… Cette complémentarité permet de détecter des bugs qui seraient passés inaperçus avec une seule approche. D’ailleurs, en parlant d’approche, on dit que le TDD adopte une approche “bottom-up” et la CI une approche “top-down”. La différence entre les deux est que le TDD va plutôt se focaliser sur des composants précis, contrairement à la CI qui va regarder le projet dans son ensemble.

Ainsi, cette complémentarité crée un “continuum de validation” qui couvre tous les aspects du développement logiciel, depuis la conception détaillée jusqu’à l’intégration globale.

1.3.2. Renforcement mutuel

Nous avons vu en quoi ces deux pratiques étaient complémentaires, en quoi leurs bénéfices pouvaient en quelque sorte s’additionner. Mais ce n’est pas tout. Un autre aspect bénéfique de leur synergie est leur renforcement mutuel.

Par exemple, sans le TDD, l’Intégration Continue perd beaucoup en intérêt. Son potentiel se trouve limité à quelques tâches d’automatisation, toute sa partie prévention de bugs disparaît. Inversement, sans la CI, le TDD se retrouve quelque peu fragilisé car “non supervisé”. L’exécution des tests est alors manuelle, il n’y a plus de vérification systématique pour s’assurer que chaque modification ne fait pas régresser le projet. Si pour l’humain, oublier d’exécuter et de créer des tests est plutôt facile, pour la CI, cela ne l’est pas. Elle n’oublie pas et impose des règles strictes configurables à souhait.

Ce renforcement mutuel est donc principalement caractérisé par une discipline renforcée par les outils apportés par ces deux pratiques. En conséquence, la confiance que l’on attribuera aux modifications apportées sur un projet adoptant le TDD et la CI, se trouvera bien plus importante que sur un projet n’employant qu’une des deux pratiques.

Voici à quoi ressemble un pipeline typique d’Intégration Continue incluant l’exécution des tests :

Illustration 4: Intégration continue via GitHub Actions

En créant un environnement propice à la création de code qualité limitant les bugs, mais également en automatisant un certain nombre de processus, cette combinaison permet de réduire l’effort nécessaire pour obtenir un projet de qualité. C’est justement ce que nous allons explorer plus en détail dans le second chapitre de ce mémoire, c’est-à-dire, en quoi cette synergie représente un gain de temps pour les développeurs.

Chapitre 2 : Gains de temps pour les développeurs

Le temps est et a toujours été un enjeu majeur dans le monde professionnel. C’est une problématique à facteurs multiples à laquelle doivent composer les entreprises. Le domaine du génie logiciel, du développement informatique, n’y échappe pas. Nombreux sont les projets reportés et sortis trop tôt, le milieu vidéoludique en est un parfait exemple. Prenez Ubisoft, le fleuron français du jeu vidéo (en tout cas, à l’heure où ce mémoire est rédigé). Chaque sortie de jeu est rythmée par la pression des investisseurs, résultant en des jeux de qualité variable, sortis trop tôt et contenant une multitude de bugs en tout genre. Depuis la sortie du premier Watch Dogs en 2013, l’entreprise ne fait qu’enchaîner les sorties ratées ou ternies par des problèmes techniques. Aujourd’hui, désormais au bord du gouffre, Ubisoft est menacée de liquidation et doit se restructurer pour regagner la confiance du public.

Pour en revenir à notre sujet, chaque gain de temps est appréciable. Comme nous le verrons un peu plus tard, la correction de bugs représente une partie non négligeable du temps que consacrent les développeurs à leur projet. Face à cette situation, l’adoption conjointe du TDD et de l’Intégration Continue propose justement de réduire ces temps morts. Toutefois, il faut reconnaître que l’investissement initial est relativement conséquent. D’ailleurs, c’est justement ce qui repousse certains développeurs à adopter ces méthodologies. Mais croyez-le, les bénéfices à moyen et long terme sont indéniables. Comme nous le verrons dans ce chapitre, le gain de temps, s’il n’est pas immédiat, vient au fil du temps au travers de plusieurs facteurs.

2.1. Réduction du temps passé à déboguer

Déboguer une application peut prendre beaucoup de temps. De toutes les tâches et activités auxquelles nous devons nous confronter dans le développement de logiciels, elle peut être la pire. C’est la plus ingrate, la plus longue, la plus frustrante, et selon le niveau de complexité du projet traité, le débogage a de quoi dégoûter un développeur de son travail. De façon tout à fait personnelle, si je suis aussi amer vis-à-vis de cette pratique, c’est que je l’ai moi-même subie. En effet, ayant dû porter une API de remboursement communiquant avec divers prestataires de paiement durant mon alternance, sans avoir eu à ce moment-là une expertise dans le développement logiciel, je peux vous affirmer que c’est ce qu’il y a de pire.

Pour appuyer mes propos, une étude estime qu’entre 30% et 50% du temps de développement est consacré à la chasse aux bugs (An Exploratory Study of How Developers Seek, Relate, and Collect Relevant Information during Software Maintenance Tasks, 2006). La combinaison TDD-CI propose plusieurs outils pour réduire drastiquement ce temps.

2.1.1. Détection précoce des erreurs

Un des outils proposés par la synergie TDD-CI est la détection précoce des erreurs. En effet, TDD reposant sur l’approche “d’échouer en premier” (fail first approach), cela limite naturellement la présence de bugs divers dans les projets développés. Au lieu de découvrir les bugs en production, ils sont identifiés dès leur apparition. Voici quelques-uns des avantages liés à la détection précoce des erreurs :

  1. Périmètre d’investigation réduit : Lorsqu’un test échoue dans un cycle TDD, le développeur sait précisément quelle fonctionnalité est concernée et quelle partie du code est à examiner.

  2. Contexte encore frais en tête : Le débogage intervient alors que vous travaillez encore sur le composant. Vous avez encore toute la logique en tête, pas besoin de passer du temps à vous remettre dans le contexte et à débugger pour trouver l’origine du problème.

  3. Tests comme spécifications : Comme précédemment expliqué, les tests TDD vous indiquent précisément ce que le code est censé faire. C’est comme avoir une documentation vivante qui vous dit exactement ce qui ne fonctionne pas.

  4. Isolation des composants : Les tests unitaires isolent naturellement les composants, ce qui simplifie l’identification de la source des problèmes.

Cette détection précoce permet de réduire le temps de correction des bugs. On estime ce gain de l’ordre de 60 à 90% (Test-driven development as a defect-reduction practice)(2003). C’est un gain de temps assez significatif qui est le bienvenu pour toute équipe.

De plus, selon sa configuration, l’intégration continue peut venir renforcer cette détection précoce en détectant les soucis liés à :

  • Des dépendances implicites oubliées
  • Des différences de configuration entre l’environnement de développement et de production
  • Des interactions imprévues avec du code développé par des collègues

Mais la détection précoce des erreurs n’est pas le seul gain de temps offert par cette combinaison.

2.1.2. Localisation précise des problèmes

En rapport avec le périmètre d’investigation réduit, la combinaison TDD-CI offre des mécanismes assez efficaces pour localiser les problèmes avec précision.

Dans un premier temps, il faut savoir qu’un test unitaire correctement réalisé est ultra spécifique. Ne devant tester qu’une fonctionnalité, s’il échoue, nous saurons directement quelle partie du code est problématique. Ensuite, il est préférable de ne faire qu’un petit nombre de modifications entre chaque intégration. L’idée est de ne pas se retrouver dans une situation où les changements sont importants et où on ne sait plus vraiment ce que nous avons modifié. En cas d’échec lors de l’exécution des tests, un rollback sera alors plus compliqué.

La traçabilité offerte par les systèmes d’intégration continue tels que GitLab CI/CD ou bien encore GitHub aide également à la localisation des problèmes. En effet, en donnant des tags permettant de déterminer le statut d’exécution des tests sur chaque commit, il est devenu facile de déterminer quelle modification s’est trouvée être problématique.

De manière générale, avoir une certaine précision dans la détection des bugs réduit le temps de débogage. Retrouver un bug caché dans un contrôleur appelé depuis un fichier routeur rempli de “switchs-case”, lui-même appelé depuis un fichier Apache htaccess qui réécrit l’URL, est un véritable enfer.

2.1.3. Études quantitatives sur le temps de débogage

Petit aparté sur le mémoire. Trouver des études qui ont quantifié l’impact du TDD et de la CI sur le temps de débogage n’a pas été une mince affaire. Non pas que les études manquent, mais comme ces études sont toutes en anglais et que le sujet est relativement vaste, je n’ai pas pu me permettre de toutes les lire. Toutefois, quelques-unes semblaient revenir assez fréquemment durant mes recherches. Ainsi, parmi celles que j’ai trouvées intéressantes, nous avons :

  1. Realizing quality improvement through test driven development: results and experiences of four industrial teams (2008) : Une étude assez intéressante menée conjointement chez Microsoft et IBM à l’époque de MSN et de Windows XP, qui a montré une réduction de 40 à 90% des bugs dans les projets utilisant le TDD (Nachiappan Nagappan & E. Michael Maximilien & Thirumalesh Bhat & Laurie Williams, 2008). Moins de bugs = moins de temps à déboguer.

  2. Evaluating advantages of test driven development: A controlled experiment with professionals (2006) : Une étude révélant que les développeurs utilisant le TDD passent en moyenne 10 à 15% moins de temps à déboguer que leurs collègues utilisant des approches traditionnelles.

  3. The Effects of Test-Driven Development on External Quality and Productivity: A Meta-Analysis (2013) : En analysant 27 études sur le TDD, les auteurs ont conclu à une réduction moyenne de 29% du taux de bugs.

Pour l’Intégration Continue spécifiquement, j’ai trouvé une étude qui a porté sur l’analyse de près de 34 000 dépôts GitHub open source, “Usage, costs, and benefits of continuous integration in open-source projects” (2016), et qui démontre clairement que les projets utilisant la CI résolvent les problèmes signalés environ 22% plus rapidement.

Voici un tableau récapitulatif des résultats de ces différentes études :

Étude Contexte Réduction du temps de débogage Méthodologie
Realizing quality improvement through test driven development: results and experiences of four industrial teams (2008) Microsoft & IBM 40-90% Comparaison de projets avec/sans TDD
Evaluating advantages of test driven development: A controlled experiment with professionals (2006) Développeurs universitaires 10-15% Expérimentation contrôlée
The Effects of Test-Driven Development on External Quality and Productivity: A Meta-Analysis (2013) Méta-analyse 29% (taux de bugs) Synthèse de 27 études
Usage, costs, and benefits of continuous integration in open-source projects (2016) Projets open source 22% (temps de résolution) Analyse de 34 000+ projets GitHub

Ces gains sont d’autant plus impressionnants dans certains contextes :

  1. Projets complexes : Plus votre système est complexe, plus le gain est important. Windows XP et MSN en sont de parfaits exemples.

  2. Équipes dispersées : Quand vous travaillez avec des collègues aux quatre coins du monde, la capacité à communiquer précisément les problèmes via les rapports de CI devient importante.

  3. Code legacy : Un cas assez complexe… L’introduction progressive de tests et de CI crée une sorte de filet de sécurité qui facilite le refactoring. Toutefois, cela peut parfois être relativement compliqué, notamment si l’architecture logicielle est assez complexe et que personne n’ose y toucher.

2.1.4. Réduction de la dette technique

La réduction de la dette technique peut ne pas paraître comme étant un gain de temps. Or, c’en est un et je dirai même qu’il est stratégique. C’est un aspect souvent négligé et pourtant important. En effet, un projet assez âgé, non documenté et dont les concepteurs initiaux ne font plus partie de l’entreprise, est toujours complexe à maintenir. Nombreux sont les bugs que personne ne veut corriger car cela nécessiterait beaucoup de débogage. À moins qu’ils ne soient critiques, il peut arriver qu’ils soient ignorés, jusqu’au jour où le composant concerné est porté sur une nouvelle technologie.

La combinaison TDD-CI brise ce cercle vicieux de plusieurs façons :

  1. Solutions propres dès le départ : Le TDD vous pousse à concevoir des solutions testables et modulaires.

  2. Refactoring continu : La phase “refactor” du cycle TDD, sécurisée par vos tests, vous permet d’améliorer constamment la qualité du code sans crainte.

  3. Prévention des régressions : La CI bloque l’introduction de régressions qui pourraient vous pousser à des correctifs hâtifs (des hotfix de hotfix…).

  4. Visibilité de la qualité : Les métriques intégrées aux pipelines CI (couverture de test, etc.) rendent la dette technique visible et gérable.

La réduction du temps de débogage n’est donc pas qu’un simple gain de temps pour les développeurs, c’est aussi un investissement stratégique dans la qualité du projet.

2.2. Optimisation des cycles de développement

Comme nous l’avons précédemment vu, au-delà du simple débogage, la combinaison TDD-CI transforme l’ensemble du cycle de développement.

2.2.1. Feedbacks rapides grâce à l’automatisation

En effet, parmi l’une de ces transformations, nous avons la mise en place de boucles de feedback automatisées qui vous informent immédiatement sur la qualité de vos modifications. Avec l’avènement de l’intelligence artificielle, des outils comme CodeRabbit vous permettent d’avoir de véritables retours pertinents dans les merge requests.

Illustration 5: Retour CodeRabbit

Ou bien encore de façon plus traditionnelle, des outils de vérification de couverture de tests tels que Codecov peuvent vous fournir des rapports détaillés sur vos modifications.

Illustration 6: Capture d'écran d'un rapport Codecov sur GitHub

L’impact de ces feedbacks rapides est multiple :

  • Moins de travail en cours
  • Apprentissage accéléré
  • Confiance accrue
  • Meilleure priorisation

Il est également important de noter que ces feedbacks facilitent également la tâche de revue des modifications par d’autres développeurs dans le cadre des merge requests.

2.2.2. Réduction des temps d’attente

L’un des problèmes les plus récurrents dans les cycles traditionnels est l’accumulation de temps d’attente entre l’accomplissement d’une tâche et sa mise en production. Dans certaines entreprises, les tâches sont encore traditionnellement testées par une personne de l’équipe. Le problème étant qu’hormis l’importante charge de travail que cela représente pour cette personne, si elle vient à s’absenter un certain temps (pour quelconque raison), la quantité de tâches terminées à tester peut s’accroître de manière effrayante. Cela a pour effet de bloquer, ou tout du moins ralentir fortement la mise en production de nouvelles tâches au profit des plus urgentes.

La combinaison TDD-CI s’attaque directement à cette problématique en supprimant ces phases de test distinctes. Étant donné que chaque modification apportée est testée individuellement, il n’y a plus besoin d’attendre la disponibilité de testeurs et testeuses pour s’assurer de leur bon fonctionnement.

Un autre avantage est la réduction des délais d’intégration offerte par la suppression pure et simple des actions manuelles. L’Intégration Continue se charge d’effectuer ces tâches ingrates. Même si le processus reste synchrone, nous n’avons ici aucun problème de coordination puisque le pipeline suit un ordre d’exécution précis préalablement configuré par les développeurs. Les périodes de “gel du code” où tout le monde attend n’existent plus, ne laissant dans les cas les plus complexes, qu’une file d’attente fiable où les intégrations s’exécuteront les unes après les autres dans les meilleurs délais.

Pour terminer, je reviendrai sur le gain de temps proposé par les outils d’analyse et assistants propulsés par l’intelligence artificielle, aux réviseurs des merge requests. L’analyse de grosses tâches pouvant prendre du temps, ces outils sont les bienvenus dans leur démarche d’amélioration de la qualité du code.

2.2.3. Élimination des tâches manuelles répétitives

Pour revenir rapidement sur la suppression des actions manuelles, l’intégration continue permet de gagner du temps en épargnant l’exécution de plusieurs tâches à chaque commit pour les développeurs.

Pour vous donner une idée concrète de l’économie réalisée, prenons l’exemple d’un développeur travaillant sur une application Java de taille moyenne :

Tâche manuelle Fréquence Temps par occurrence Temps hebdomadaire
Exécution de tests 10-15 fois/jour 2-5 minutes 100-375 minutes
Compilation complète 3-5 fois/jour 1-3 minutes 15-75 minutes
Déploiement en test 2-3 fois/jour 5-15 minutes 50-225 minutes
Vérifications qualité 1-2 fois/jour 3-8 minutes 15-80 minutes
Total 180-755 minutes

Ce tableau montre qu’un développeur peut économiser entre 3 et 12.5 heures par semaine simplement en automatisant ces tâches répétitives. Pour une équipe de 10 personnes, cela représente potentiellement 30 à 125 heures hebdomadaires récupérées.

2.2.4. Optimisation du travail collaboratif

Au-delà des gains individuels, la combinaison TDD-CI transforme également la manière dont les équipes collaborent. Voici quelques-uns des gains apportés :

  1. Intégration continue des contributions : Les conflits de code sont détectés et résolus rapidement, évitant les situations où plusieurs développeurs doivent passer du temps à fusionner des changements divergents.

  2. Visibilité partagée : Les tableaux de bord CI offrent une vue d’ensemble instantanée sur l’état du code.

  3. Transfert de connaissances facilité : Les tests TDD servent de documentation vivante, ce qui facilite énormément l’intégration des nouveaux membres dans l’équipe.

  4. Environnements standardisés : La définition des environnements dans le pipeline CI (souvent via Docker) élimine les “ça marche chez moi” et tous les problèmes qui en découlent.

Ces optimisations sont précieuses dans les équipes réparties aux 4 coins du monde ou travaillant en mode asynchrone.

L’expérience d’Etsy, documentée par Patrick McDonnell en 2012, est assez révélatrice. En effet, après l’adoption du TDD et de la CI, le temps moyen pour qu’un nouveau développeur fasse sa première contribution est passé de 30 à 11 jours. Ce temps d’apprentissage réduit prouve que les avantages du TDD-CI sont multidimensionnels et bénéficient à l’ensemble de l’équipe.

2.2.5. Impact sur les délais de livraison

Pour conclure sur l’aspect optimisation des cycles de développement, je terminerai sur les gains globaux apportés sur les délais de livraison. En effet, grâce aux diverses optimisations présentées ci-dessus, les temps de livraisons se trouvent logiquement réduits comparativement à un projet développé de manière plus traditionnelle.

De manière tout à fait personnelle, depuis que l’entreprise dans laquelle j’effectue mon alternance a migré son architecture interne vers Google Cloud en 2024, j’ai pu observer que le nombre de tâches mises en production a doublé. La raison à cela est l’automatisation et la sécurisation des pipelines de mises en production. Là où avec le précédent processus de mise en production il ne fallait rien merger à la branche principale pendant les mises en production, aujourd’hui, cela n’a plus d’autres impacts que de simplement agrandir la file d’attente de mise en production. Les problèmes de versions du code divergentes entre les serveurs n’existent plus. Désormais, tout est synchrone.

Le rapport “State of DevOps” de 2019 conforte mes propos. En effet, il rapporte que les entreprises et organisations ayant atteint un niveau élevé de maturité dans les pratiques incluant TDD et CI, déployaient en moyenne 208 fois plus fréquemment que leurs homologues moins avancées. Bien que ce gain soit à nuancer selon les entreprises, nous pouvons noter un gain certain suite à l’adoption de ces pratiques.

L’optimisation des cycles de développement grâce au TDD et à la CI représente donc bien plus qu’un simple gain de temps, c’est une transformation de la manière dont vous créez et livrez du logiciel.

2.3. Impact sur la productivité et la satisfaction des développeurs

Jusque-là, nous avons principalement étudié les gains de temps apportés par l’adoption du TDD et de la CI au travers des différentes optimisations proposées. J’aimerais maintenant terminer ce chapitre sur un point plus personnel qui me paraît tout à fait pertinent, et qui semble être également partagé par l’ensemble de la communauté des développeurs. Ainsi, dans cette dernière partie du second chapitre, nous étudierons l’impact du TDD et de la CI sur la productivité et la satisfaction des développeurs. Bien que ce soient des effets difficiles à mesurer, ils ne sont pas à négliger car il peut en découler plusieurs conséquences comme par exemple, la rétention des talents et la performance de l’équipe sur le long terme au sein d’une entreprise.

2.3.1. Confiance accrue des développeurs

Pour commencer, la synergie TDD-CI renforce la confiance des développeurs vis-à-vis des modifications qu’ils apportent au sein des projets. Nous l’avions rapidement vu dans la présentation du TDD et dans la synthèse des gains proposés par les feedbacks rapides, sans être entré dans les détails.

Par quoi se traduit cette confiance accrue ? Eh bien plusieurs actions, plusieurs facteurs permettent de la mesurer. Dans un premier temps, elle se traduit par une réduction de l’anxiété face aux changements. Comme précédemment expliqué, les tests automatisés constituant un filet de sécurité permettant de modifier le code sans avoir peur de “casser quelque chose”, les développeurs se trouvent plus confiants dans les modifications qu’ils apportent.

Mais ce n’est pas tout. Ce filet de sécurité matérialisé par les tests permet de tester d’autres solutions qui n’auraient pas pu être implémentées dans un processus plus traditionnel, ou tout du moins avec plus de difficulté. Parmi les avantages offerts par cette liberté d’expérimentation, on peut retrouver une meilleure capacité à innover, à optimiser le code logiciel afin d’offrir de meilleures performances, et il en résulte donc de manière plus ou moins directe, l’amélioration de l’expérience utilisateur.

2.3.2. Amélioration de la qualité perçue des projets

L’amélioration de la qualité perçue des projets va de pair avec la confiance apportée aux développeurs. En effet, les pratiques TDD-CI étant des méthodologies relativement strictes et favorisant l’écriture de code de qualité, les modifications apportées paraissent de “meilleure qualité”. Le code paraît plus robuste et plus résistant à l’épreuve du temps, là où un code non testé paraît beaucoup plus fragile car développé sur des bases peu voire pas testées.

De plus, les métriques proposées par l’Intégration Continue telles que le taux de couverture en tests du projet permettent de valoriser objectivement la qualité des modifications apportées.

2.3.3. Impact sur l’attractivité et la rétention des développeurs

Enfin, je terminerai ce second chapitre sur l’impact du TDD et de la CI sur l’attractivité des entreprises et leur capacité à garder les développeurs.

Selon une étude de LinkedIn réalisée en 2018, les entreprises identifiées comme ayant des pratiques d’ingénierie avancées recevaient en moyenne 2.3 fois plus de candidatures spontanées et affichaient des taux de rétention supérieurs de 34%. Dans un marché où chaque recrutement peut prendre des mois, c’est un avantage compétitif à ne pas négliger. En gardant leurs développeurs plus longtemps, les entreprises économisent du temps et de l’argent.

Ainsi, l’impact du TDD et de la CI sur la productivité et la satisfaction des développeurs va au-delà des gains de temps. En transformant l’expérience du développement logiciel, le TDD et la CI réduisent le stress, augmentent la confiance et améliorent la qualité perçue du travail effectué, des modifications apportées.

Dans le chapitre suivant, nous verrons comment ces gains d’efficacité se traduisent concrètement par une amélioration mesurable de la fiabilité et de la qualité des projets logiciels.

Chapitre 3 : Amélioration de la fiabilité des projets

Jusque-là, nous avons vu en quoi l’association du Test-Driven Development et de l’Intégration Continue permet de réduire le temps passé à déboguer, mais également à optimiser les cycles de développement. Cependant, nous sommes en droit de nous demander si toute cette complexité apportée est bien bénéfique pour les projets vis-à-vis de leur fiabilité. En réalité, les précédentes parties nous ont déjà donné quelques éléments de réponse. Mais dans ce chapitre, nous allons voir plus en détail en quoi cette affirmation est vraie.

3.1. Qualité du code

L’un des bénéfices les plus importants de cette association est sans aucun doute l’amélioration de la qualité du code apporté par les développeurs. Elle est fondamentale pour garantir la fiabilité, la maintenabilité et l’évolutivité des projets, et est mesurable au travers de plusieurs métriques.

3.1.1. Métriques de qualité

Dans un premier temps, pour évaluer objectivement l’impact du TDD et de la CI sur la qualité du code, il est nécessaire de s’appuyer sur des métriques concrètes. Parmi les indicateurs les plus pertinents, on retrouve la complexité cyclomatique, le couplage, la cohésion, la duplication de code et bien entendu, la dette technique.

La complexité cyclomatique

Développée par Thomas McCabe en 1976, la complexité cyclomatique permet de mesurer le nombre de chemins d’exécution indépendants dans un programme. Pour faire simple, elle reflète à quel point votre code est complexe, et potentiellement difficile à tester.

Illustration 7: Représentation de la complexité cyclomatique

Plus le code est complexe, plus il risque de contenir des bugs. La multiplication de conditions opératoires imbriquées est généralement une mauvaise idée en raison du nombre de tests à réaliser afin de pouvoir toutes les couvrir. On préférera découper le code en plusieurs méthodes afin de respecter le principe de responsabilité unique, simplifiant ainsi sa lecture et sa compréhension.

Couplage

Le couplage permet de mesurer le niveau d’interaction, d’interdépendance entre les différents modules d’un système. On dit que deux modules sont couplés s’ils échangent de l’information. On parlera donc de couplage fort si les composants en échangent beaucoup, et de couplage faible s’ils en échangent peu. Un couplage élevé rend le code plus difficile à modifier sans créer d’effets de bord indésirables

Prenons un exemple concret.

Voici une classe PHP s’apparentant à un service de conversion d’images.

class ImageTranscodingService
{
    protected ImageManager $imageManager;

    public function __construct(ImagickDriver $driver)
    {
        $this->imageManager = new ImageManager($driver);
    }

    /**
     * Transcode an image to a new resolution
     *
     * @param  string  $source  The source image path or content. Eg: /path/to/image.jpg or file_get_contents('/path/to/image.jpg')
     * @param  int|null  $resolution  The new resolution to transcode the image to
     * @param  string  $codec  The codec to use for transcoding. Eg: jpeg, webp, png, avif
     * @return string|null The transcoded image content
     */
    public function transcode(string $source, ?int $resolution = null, string $codec = 'avif'): ?string
    {
        $image = $this->imageManager->read($source);
        try {
            $imageArea = $image->width() * $image->height();
            $maxArea = Imagick::getResourceLimit(Imagick::RESOURCETYPE_AREA);
            $imageWidth = $image->width();
            $maxWidth = config('app.imagick.max_width');
            $imageHeight = $image->height();
            $maxHeight = config('app.imagick.max_height');

            if ($imageWidth > $maxWidth || $imageHeight > $maxHeight || $imageArea > $maxArea) {
                Log::error('Image resolution exceeds maximum allowed resolution', [
                    'image_height' => $image->height(),
                    'image_width' => $image->width(),
                    'image_surface' => $image->width() * $image->height(),
                    'max_width' => config('app.imagick.max_width'),
                    'max_height' => config('app.imagick.max_height'),
                    'max_surface' => Imagick::getResourceLimit(Imagick::RESOURCETYPE_AREA),
                ]);

                return null;
            }

            if ($resolution) {
                $image->scale($resolution);
            }

            return match ($codec) {
                'jpeg' => $image->encode(new JpegEncoder(quality: 85))->toString(),
                'webp' => $image->encode(new WebpEncoder(quality: 85))->toString(),
                'png' => $image->encode(new PngEncoder)->toString(),
                default => $image->encode(new AvifEncoder(quality: 85))->toString(),
            };
        } catch (RuntimeException $exception) {
            Log::error('Failed to transcode image', [
                'exception' => $exception,
            ]);

            return null;
        }
    }

    /**
     * Get the dimensions of an image
     *
     * @param  string  $source  The source image path or content. Eg: /path/to/image.jpg or file_get_contents('/path/to/image.jpg')
     * @return array{width: int, height: int}
     */
    public function getDimensions(string $source): array
    {
        $image = $this->imageManager->read($source);

        return [
            'width' => $image->width(),
            'height' => $image->height(),
        ];
    }
}

Son fonctionnement est simple. Elle possède une méthode qui permet de convertir une image dans une résolution et un format donné, ainsi qu’une autre qui retourne ses dimensions. Ces deux méthodes prennent en paramètre l’image à utiliser. Cependant, vous remarquerez que la classe n’interagit pas directement avec l’image. Pour cela, elle utilise un autre module, qui dans notre cas se nomme Imagick.

Dans notre cas, le couplage est représenté par la dépendance de notre service au module PHP qu’est Imagick. On parlera donc de couplage fort, notre code reposant entièrement sur ce module. Toutefois, ce service servant justement d’intermédiaire entre ce module et notre code, s’il advient qu’Imagick devienne un jour obsolète, son remplacement sera plus aisé grâce à son couplage faible vis-à-vis de l’ensemble du projet.

Cohésion

La cohésion permet de mesurer à quel point les éléments d’un module sont liés fonctionnellement. On dit qu’un module est cohérent s’il a un objectif clair et précis, et qu’il respecte le principe de responsabilité unique. Un module peu cohérent peut être difficile à comprendre et à maintenir.

Prenons un exemple concret pour illustrer ce concept.

Voici une classe PHP avec une faible cohésion :

class UserManager
{
    /**
     * Authenticate a user with provided credentials
     */
    public function authenticateUser(string $email, string $password): bool
    {
        // Code d'authentification
        return true;
    }
    
    /**
     * Send an email to a user
     */
    public function sendEmail(string $to, string $subject, string $content): bool
    {
        // Code d'envoi d'email
        return true;
    }
    
    /**
     * Calculate user's subscription price with taxes
     */
    public function calculateSubscriptionPrice(float $basePrice, float $taxRate): float
    {
        return $basePrice * (1 + $taxRate);
    }
    
    /**
     * Generate a PDF invoice for the user
     */
    public function generateInvoicePdf(int $userId, int $invoiceId): string
    {
        // Code de génération de PDF
        return 'invoice.pdf';
    }
}

Cette classe illustre parfaitement ce qu’il ne faut pas faire. En effet, elle effectue plusieurs tâches non liées telles que l’authentification, l’envoi d’e-mails, le calcul de prix et la génération de PDF. Ces méthodes n’ayant pas de thème commun, on dit que la classe est incohérente.

Avec une approche à forte cohésion, nous séparerions ces responsabilités en classes distinctes :

class UserAuthenticationService
{
    public function authenticate(string $email, string $password): bool
    {
        // Code d'authentification
        return true;
    }
}

class EmailService
{
    public function send(string $to, string $subject, string $content): bool
    {
        // Code d'envoi d'email
        return true;
    }
}

class SubscriptionPriceCalculator
{
    public function calculateWithTaxes(float $basePrice, float $taxRate): float
    {
        return $basePrice * (1 + $taxRate);
    }
}

class InvoiceGenerator
{
    public function generatePdf(int $userId, int $invoiceId): string
    {
        // Code de génération de PDF
        return 'invoice.pdf';
    }
}

Cette restructuration offre plusieurs avantages :

  • Chaque classe a une responsabilité unique et bien définie
  • Le code est isolé et donc plus facile à tester
  • Les modifications apportées à une méthode n’affectent pas les autres
  • Les classes peuvent évoluer indépendamment
  • La réutilisation du code est facilitée

Comme nous l’avions vu dans le premier chapitre de ce mémoire, le TDD encourage naturellement la création d’une conception modulaire. En effet, écrire des tests pour des classes à faible cohésion s’avère complexe et fastidieux. En créant les tests avant le code fonctionnel, les développeurs sont poussés à concevoir des classes plus ciblées, à responsabilité unique, et donc cohésives.

Duplication de code et dette technique

Nous en venons aux deux dernières métriques de qualité du code. En raison de leur simplicité, nous ne rentrerons pas dans les détails comme pour les précédentes métriques.

La première (la duplication de code), représente une mauvaise habitude que possèdent certains développeurs et dont nous avons tous eu recours au moins une fois dans notre vie. Du code dupliqué est du code à maintenir en plus sans que cela soit nécessaire. La philosophie DRY (Don’t Repeat Yourself) est à appliquer sur tout type de projet logiciel, elle est d’ailleurs à la base des projets conçus avec des langages tels qu’UML.

La seconde (la dette technique), représente une estimation quantitative du coût futur de la maintenance du projet, induit par des imperfections dans le code, des dépendances à des technologies dépréciées.

Ainsi nous avons vu l’ensemble des métriques concrètes utilisables pour évaluer la qualité du code d’un projet. Passons maintenant aux autres types de métriques.

3.1.2. Maintenabilité et lisibilité

Au-delà des métriques purement techniques, le TDD et la CI améliorent deux aspects cruciaux mais plus qualitatifs du code : sa maintenabilité et sa lisibilité.

En effet, les nombreuses bonnes pratiques apportées par le TDD telles que la conception modulaire, ou bien encore la documentation vivante représentée par les tests, améliorent significativement ces deux caractéristiques. Plus spécifiquement pour la lisibilité, le respect des conventions de nommage et de styles d’écriture tels que PSR12 pour PHP, aident à mieux comprendre un code dès sa première lecture.

L’étude de Nachiappan Nagappan réalisée chez Microsoft et IBM durant le début des années 2000 le confirme : on apprend que les modules développés en TDD nécessitaient en moyenne 40% moins de temps pour être compris par de nouveaux développeurs, par rapport aux modules développés de manière traditionnelle. C’est un gain assez conséquent lorsque l’on sait combien de temps un développeur peut passer à essayer de comprendre du code existant avant de pouvoir y contribuer efficacement.

3.1.3. Modularité et principes SOLID

Enfin, le respect des principes SOLID peut également être utilisé pour mesurer objectivement la qualité du code d’un projet. Le TDD encourageant fortement leur application, en général, les projets s’y conforment au moins en partie. Mais que sont les principes SOLID ?

SOLID est l’acronyme de Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle et Dependency Injection Principle. Inventés, ou tout du moins, formulés par Robert C. Martin en 2000, les principes SOLID représentent une série de bonnes pratiques à suivre pour le développement de logiciel. Voyons chacun de ces principes en détail.

  1. Principe de responsabilité unique (SRP) : Pour être facilement testable, une classe ou un module doit généralement avoir une responsabilité bien définie et limitée. J’ai souvent constaté que les tests unitaires difficiles à écrire sont un excellent indicateur de classes qui en font trop et qui devraient être découpées.

  2. Principe ouvert/fermé (OCP) : Le TDD facilite les extensions sans modification du code existant, car les nouvelles fonctionnalités sont ajoutées en écrivant d’abord de nouveaux tests.

  3. Principe de substitution de Liskov (LSP) : Les tests écrits pour une classe de base doivent fonctionner pour ses dérivées, ce qui renforce naturellement ce principe.

  4. Principe de ségrégation des interfaces (ISP) : Pour faciliter les mocks dans les tests, nous sommes amenés à créer des interfaces plus fines et spécifiques.

  5. Principe d’inversion des dépendances (DIP) : L’injection de dépendances, essentielle pour des tests unitaires efficaces, implémente directement ce principe.

Grâce à cette modularité, il devient beaucoup plus facile d’isoler et de corriger des bugs. De plus, les composants du projet sont naturellement plus réutilisables, et il devient plus aisé de faire évoluer des parties du système indépendamment, sans faire régresser le code du projet. D’ailleurs, la réduction des régressions est un second avantage proposé par la combinaison TDD-CI.

3.2. Réduction des régressions

La réduction des régressions est un second avantage proposé par cette synergie. En effet, cette omniprésence des tests au sein du cycle de développement, permet justement d’éviter les situations où les modifications apportées font apparaître des bugs sur les fonctionnalités déjà présentes. Dans cette partie, nous allons voir comment mesurer la capacité d’un projet à prévenir les régressions, comment s’en prémunir, et quels outils peuvent nous aider dans cette tâche.

3.2.1. La couverture de test comme indicateur de fiabilité

La couverture de test représente un indicateur clé de la capacité d’un projet à prévenir les régressions. Son principe est relativement simple, elle mesure la proportion du code source exécutée pendant les tests. Pour la mesurer, il existe différentes métriques ayant chacune leur spécificité, mais qui sont toutes complémentaires.

La première est la plus simple, il s’agit de la couverture de lignes. Elle consiste à mesurer le pourcentage de lignes de code exécutées par les tests, c’est la métrique la plus couramment utilisée. Les autres métriques sont plus complexes car elles nécessitent d’analyser le code plus en profondeur. Ainsi, nous avons la couverture de branches, qui consiste à mesurer le pourcentage de branches conditionnelles “if/else” (et les switch) concernées par les tests. Nous avons aussi la couverture de fonctions, qui, de la même manière, mesure le pourcentage de méthodes/fonctions appelées pendant les tests. Puis, la couverture de condition qui elle, représente une forme un peu plus complexe que la couverture de branches puisque cette métrique se charge de vérifier les sous expressions booléennes.

Par exemple, dans if (a && b), cette métrique vérifie les cas où a est vrai/faux et b est vrai/faux.

Enfin, nous trouvons la couverture de chemin, qui est de loin la plus sophistiquée. Cette métrique évalue la proportion des chemins d’exécution possibles couverts par les tests.

Le TDD, de par sa nature même, tend à produire une couverture de test élevée, puisque chaque fonctionnalité est développée en commençant par un test. Cependant, il faut savoir que cette couverture n’est intéressante que si les tests sont pertinents et vérifient les comportements attendus. Comme le souligne Martin Fowler, “La couverture de test est un indicateur utile de zones potentiellement sous-testées, mais une couverture élevée n’est pas une garantie de tests de qualité.” Des tests mal conçus peuvent exécuter le code sans vérifier efficacement son comportement.

L’intégration continue joue un rôle crucial en :

  1. Mesurant et surveillant systématiquement la couverture de test
  2. Empêchant sa dégradation au fil du temps (via des seuils minimaux configurables)
  3. Rendant visible l’évolution de cette métrique à travers le temps

Des outils comme Codecov permettent de visualiser précisément quelles parties du code sont couvertes ou non, et comment cette couverture évolue entre les commits. Voici un exemple de rapport Codecov montrant du code insuffisamment testé.

Illustration 8: Rapport de couverture Codecov montrant des lignes manquantes

3.2.2. Prévention des bugs de régression

Nous avons vu en quoi consiste la couverture de test, pourquoi elle représente un indicateur de fiabilité. Voyons maintenant comment nous pouvons nous prémunir des bugs de régression au sein d’un projet.

Pour commencer, revenons à la définition même d’un bug de régression. On dit qu’un bug fait régresser la fiabilité d’un projet, s’il fait revenir un bug, un problème, un défaut, qui était autrefois corrigé. La différence avec un simple bug est assez subtile. On ne parle de régression que lorsqu’il s’agit d’un problème dont la correction était considérée comme acquise. Sa réapparition peut résulter en un manque de test concernant un cas particulier. L’une des actions les plus logiques est donc de mettre en place des tests qui permettent justement de reproduire ce bug, avant même de le corriger. Cela s’inscrit dans la méthodologie TDD et c’est une habitude à prendre qui transforme chaque bug, chaque problème, en une opportunité de renforcer la suite de tests. Ainsi, on s’assure d’avoir bien compris le bug, mais on garantit également sa disparition définitive.

Maintenant que nous avons cette première ligne de défense établie, nous venons la renforcer avec l’intégration continue qui va exécuter tous nos tests. Agissant comme un chien de garde (watchdog en anglais), l’intégration continue va s’assurer qu’aucun test n’échoue, prévenant ainsi les erreurs humaines pouvant être liées à divers facteurs tels que l’oubli d’exécuter les tests du projet.

Une manière un peu plus technique de prévenir les bugs de régression est d’effectuer des tests paramétrés. En effet, ces tests vérifient le comportement d’une fonctionnalité avec de multiples jeux de données. L’idée est d’augmenter la robustesse d’un système face aux cas particuliers, en effectuant des assertions sur un même système avec des données divergentes. L’illustration ci-dessous illustre très bien le principe de fonctionnement de ce type de tests.

Illustration 9: Iluustration de test paramétré

Pour les projets plus sophistiqués, il existe le property-based testing qui représente une extension du TDD. C’est une technique assez complexe qui consiste à définir des propriétés que le code doit respecter, puis de générer automatiquement des jeux de données. La courbe d’apprentissage de cette technique est plus abrupte, on préférera l’utiliser sur des projets requérant une grande stabilité.

Ainsi nous avons vu comment nous pouvons prévenir l’apparition de bugs de régression. Avant de terminer ce troisième chapitre portant sur l’amélioration de la fiabilité des projets, passons à la présentation de quelques-uns des outils d’analyse de couverture de code.

3.2.3. Outils d’analyse de couverture

Selon les technologies employées, il peut exister de nombreux outils permettant de mesurer la couverture des projets en tests. Pour des raisons de simplicité, nous nous pencherons sur les plus populaires, et verrons un cas d’usage réel issu d’un projet que j’ai moi-même développé. Ainsi, parmi les outils d’analyse les plus fréquemment utilisés, nous retrouvons :

  1. Codecov : Plateforme cloud développée par Sentry, particulièrement populaire chez les projets open source

  2. JaCoCo : Outil spécialisé dans le développement d’application Java et Kotlin

  3. PHPUnit Code Coverage : Extension de PHPUnit pour générer des rapports de couverture pour les applications PHP.

  4. gcov/lcov : Outils historiques pour les langages C/C++, toujours largement utilisés et intégrables aux pipelines CI modernes.

Ces outils offrent des fonctionnalités avancées comme :

  • La visualisation de la couverture directement dans le code source
  • L’identification des points chauds (zones fréquemment modifiées mais peu testées)
  • L’analyse différentielle de couverture entre les commits
  • L’intégration avec les pull/merge requests pour vérifier la couverture des changements

L’intégration de ces outils dans un pipeline CI peut être illustrée par cet extrait de configuration GitLab CI que j’ai utilisé sur un projet :

default:
  image: registry.gitlab.sl-projects.com/sofianelasri/rann-graphic-design-website:latest

stages:
  - test
  - coverage
  - deploy

unit_test:
  stage: test
  script:
    - cp .env.testing .env
    - composer install --prefer-dist --no-interaction
    - php artisan key:generate
    - php artisan migrate
    - npm install
    - npm run build
    - php artisan test

codecov:
  stage: coverage
  script:
    - cp .env.testing .env
    - composer install --prefer-dist --no-interaction
    - php artisan key:generate
    - php artisan migrate
    - npm install
    - npm run build
    - XDEBUG_MODE=coverage php artisan test --coverage-clover=coverage.xml
    - curl -Os https://uploader.codecov.io/latest/linux/codecov
    - chmod +x codecov
    - ./codecov -t ${CODECOV_TOKEN}
  artifacts:
    paths:
      - coverage.xml
    expire_in: 1 week
  variables:
    CODECOV_ENV: gitlab
    CI_JOB_ID: ${CI_JOB_ID}
    CI_JOB_NAME: ${CI_JOB_NAME}
    CI_COMMIT_SHA: ${CI_COMMIT_SHA}
    CI_COMMIT_REF_NAME: ${CI_COMMIT_REF_NAME}
    CI_REPOSITORY_URL: ${CI_REPOSITORY_URL}

deploy_production:
  stage: deploy
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends openssh-client
    - composer global require "laravel/envoy"
  script:
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - ssh-add <(echo "$SSH_PRIVATE_KEY")
    - mkdir -p ~/.ssh
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
    - /config/composer/vendor/bin/envoy run deploy --commit="$CI_COMMIT_SHA"
  environment:
    name: production
    url: https://rann-graphic-design.fr/
  when: on_success
  rules:
    - if: $CI_COMMIT_BRANCH == "master"

Cette configuration illustre comment un rapport de couverture PHPUnit peut être généré lors des tests, puis transformé et envoyé à Codecov pour analyse et visualisation. Vous noterez que la partie déploiement ne s’exécute que si l’exécution des tests a réussi.

Ainsi, nous avons vu en quoi la combinaison TDD-CI permet une réduction des régressions au sein des projets.

Conclusion

Nous voilà arrivé au terme de ce mémoire. Après avoir établi les principes de ceux deux méthodologies que sont le Test Driven Development et l’Intégration Continue, nous pouvons affirmer qu’elles représentent bien plus qu’une tendance dans le monde du développement logiciel. En effet, elles représentent une véritable révolution dans la manière de travailler sur des projets logiciels, de leur conception à leur maintenance.

Nous avons vu que la synergie proposée par l’association du TDD et de la CI répond à notre problématique au travers de divers outils, divers pratiques, applicables et utilisables pour chaque axe. Par exemple, pour ce qui est de la réduction du temps de développement, nous avons démontré que contrairement aux idées reçu, l’investissement initial en termes de temps est largement compensé par les gains ultérieurs. Des pratiques telles que la détection précoce des bugs, l’automatisation des tâches répétitives ou bien encore l’optimisation des cycles de développement, contribuent à réduire durablement le temps passé sur un projet tout en améliorant sa fiabilité sur le long terme.

D’ailleurs, de façon plus spécifique, la fiabilité des projets est également améliorée grâce à des métriques comme la couverture des projets en tests et l’exécution automatisées des tests, réduisant ainsi les régressions.
Enfin, nous avons montré en quoi ces pratiques permettant l’amélioration de la maintenabilité des projets. Que ce soit au travers de la documentation vivante formée par les tests, ou bien encore par le respect des principes SOLID au travers du TDD, la maintenance des projets se trouve être impactée bénéfiquement par la synergie TDD-CI.
Cependant, il est important de nuancer ces bénéfices. S’il est vrai que cette synergie apporte beaucoup de positif, il convient de noter qu’en raison de la complexité des deux méthodologies, un certain temps d’adaptation peut être nécessaire avant de les maitriser. De plus, si leur implémentation, leur adoption, est mal réalisée, de nouveaux problèmes peuvent apparaître et venir compliquer l’expérience de développement.

Comme exemple, je prendrais le cas de la migration de l’architecture serveur de l’entreprise dans laquelle j’effectue mon alternance, Kang. En effet, la migration de l’ancienne infrastructure « on premise » avec des pipelines manuels, vers une infrastructure entièrement basée sur Google Cloud Platform avec des pipelines GitLab, s’avère être au mieux, un semi échec, et au pire, une très mauvaise idée qui coûte beaucoup trop d’argent par rapport aux bénéfices apportés. Sans rentrer dans les détails, il semblerait que cet échec soit caractérisé par une architecture logicielle complexe et difficile à containériser, une implémentation de l’intégration continue réalisée par une équipe tierce à l’entreprise, résultant en des choix qui auraient pu être meilleurs.

En conclusion, le TDD et la CI ne sont pas une solution miracle, mais plutôt des outils puissants. S’ils sont correctement implémentés, leurs effets sur les projets logiciels sont bénéfiques et en cela il est facile d’expliquer pourquoi leur adoption se trouve être de plus en plus populaire. Dans un contexte où la fiabilité des logiciels devient cruciale, ces méthodologies apparaissent comme essentielles au succès des projets logiciels modernes.

Références bibliographiques

Test-Driven Development: By Example

  • Auteur : Kent Beck
  • Éditeur : Addison-Wesley Professional
  • Date de publication : 17 novembre 2002

Continuous Integration: Improving Software Quality and Reducing Risk

Clean Code: A Handbook of Agile Software Craftsmanship

  • Auteur : Robert C. Martin
  • Éditeur : Prentice Hall
  • Date de publication : 1 août 2008

Realizing quality improvement through test driven development: results and experiences of four industrial teams

  • Auteurs : Nachiappan Nagappan, E. Michael Maximilien, Thirumalesh Bhat, Laurie Williams
  • Éditeur : Empirical Software Engineering Journal
  • Date de publication : 26 février 2008

The Effects of Test-Driven Development on External Quality and Productivity: A Meta-Analysis

Usage, costs, and benefits of continuous integration in open-source projects

A Complexity Measure

  • Auteur : Thomas J. McCabe
  • Éditeur : IEEE Transactions on Software Engineering
  • Date de publication : 1 décembre 1976

State of DevOps Report 2019

  • Auteurs : DORA (DevOps Research and Assessment) & Google Cloud
  • Éditeur : Google Cloud
  • Date de publication : 15 août 2019

Manifesto for Agile Software Development

Références webographiques

Unlock The Power Of TDD

How to Implement TDD in Laravel and Why it is Important?

What is CI/CD?

  • Adresse : What is CI/CD?
  • Auteur : GitLab Team
  • Date de publication : inconnue

Martin Fowler sur l’Intégration Continue

Test-Driven Development: Really, It’s a Design Technique

The Practical Test Pyramid

SOLID Principles in Laravel

Glossaire

  • TDD : Test Driven Development (ou Développement piloté par les tests en français). C’est une approche de développement logiciel qui consiste à écrire les tests unitaires avant d’écrire le code source d’une fonctionnalité. Le but est de garantir que le code source est testé et fonctionne correctement.
  • CI : Continuous Integration (ou Intégration continue en français). C’est une pratique de développement logiciel qui consiste à intégrer régulièrement les modifications du code source dans un référentiel partagé. L’objectif est de détecter et de corriger les erreurs de manière précoce.
  • CD : Continuous Delivery (ou Livraison continue en français). C’est une pratique de développement logiciel qui consiste à automatiser le processus de déploiement des applications. L’objectif est de rendre le déploiement plus rapide et plus fiable.
  • GitLab CI/CD Pipelines : C’est un outil d’intégration continue et de livraison continue fourni par GitLab. Il permet d’automatiser le processus de construction, de test et de déploiement des applications.
  • DevOps : Ensemble de pratiques qui combinent le développement logiciel (Dev) et les opérations informatiques (Ops) visant à raccourcir le cycle de développement et à fournir des fonctionnalités de haute qualité en continu.
  • Pipeline : Série d’étapes automatisées définissant le processus de livraison d’une application, de la phase de développement jusqu’à la production.
  • Refactoring : Processus de restructuration du code existant sans en modifier le comportement externe, dans le but d’améliorer sa structure interne et sa lisibilité.
  • Test unitaire : Test vérifiant le bon fonctionnement d’une unité de code isolée (généralement une fonction ou une méthode).
  • Test d’intégration : Test vérifiant que différents modules ou services fonctionnent correctement ensemble.
  • Test fonctionnel : Test vérifiant qu’une fonctionnalité répond aux exigences métier définies.
  • Test d’acceptation : Test vérifiant que le système répond aux exigences du client et qu’il peut être accepté.
  • Legacy code : Code source hérité d’une version antérieure d’un logiciel, souvent difficile à maintenir en raison de sa complexité, de son manque de documentation ou de l’absence de tests.
  • Integration hell : Situation où l’intégration des différentes parties d’un logiciel devient extrêmement difficile et chronophage en raison des divergences accumulées entre les branches de développement.
  • Régression : Bug qui fait réapparaître un problème précédemment résolu suite à une modification du code.
  • Mocking : Technique consistant à simuler le comportement d’objets réels de manière contrôlée lors des tests unitaires.
  • Couverture de code : Mesure du pourcentage de code source exécuté lors des tests, utilisée pour évaluer l’efficacité de la suite de tests.
  • Dette technique : Coût implicite du travail supplémentaire causé par le choix d’une solution facile à court terme mais sous-optimale à long terme.
  • Principes SOLID : Ensemble de cinq principes de conception orientée objet (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) visant à rendre le code plus maintenable et évolutif.
  • Feature toggles : Technique permettant d’activer ou désactiver des fonctionnalités spécifiques sans déployer de nouveau code, facilitant les déploiements progressifs.
  • Extreme Programming (XP) : Méthodologie de développement logiciel agile qui préconise la programmation en binôme, le développement piloté par les tests, et l’intégration continue.
  • Continuous Deployment : Extension du Continuous Delivery où chaque modification qui passe tous les tests est automatiquement déployée en production sans intervention humaine.
  • Infrastructure as Code (IaC) : Pratique consistant à gérer et provisionner l’infrastructure informatique via du code plutôt que par des processus manuels.
  • Complexité cyclomatique : Mesure quantitative de la complexité d’un programme, basée sur le nombre de chemins d’exécution indépendants dans le code.
  • Couplage : Degré d’interdépendance entre les modules ou composants d’un système logiciel.
  • Cohésion : Mesure du degré auquel les éléments d’un module sont fonctionnellement liés.
  • DRY (Don’t Repeat Yourself) : Principe de développement logiciel visant à réduire la répétition de code.
  • Property-based testing : Technique de test où les propriétés que le code doit respecter sont définies, puis des données de test sont générées automatiquement pour vérifier ces propriétés.
  • Build : Processus de conversion du code source en artefacts exécutables.
  • Artefact : Fichier ou ensemble de fichiers produits lors du processus de build (ex: fichier JAR, image Docker).
  • Pull/Merge Request : Mécanisme permettant aux développeurs de proposer des modifications de code pour révision avant intégration dans la branche principale.
2025 - Sofiane Lasri-Trienpont, développé sur Laravel.