François Hyvrier
May 14 · 7 min read

Démêler le code pour le rendre testable

On est rarement à l’aise à l’idée de modifier une application qu’on connaît mal : on a peur de casser une fonctionnalité existante en en implémentant une nouvelle ou en corrigeant un bug.

Impression : modification d’une fonctionnalité dans du code legacy.

Les tests unitaires sont d’un grand réconfort dans cette situation. S’ils sont suffisamment exhaustifs, ils constituent un bon filet de sécurité pour le développeur qui peut rapidement vérifier si ses modifications ont changé le fonctionnement attendu de l’application.

Seulement, tous les codes ne se prêtent pas forcément bien aux tests. Celui-ci par exemple :

Ce bout de code est vraiment extrait d’une de nos applications legacy et mériterait un léger rafraîchissement. Mais avant de se lancer dans un joyeux nettoyage de printemps, il serait tout de même plus sécurisant d’avoir quelques tests unitaires sur lesquels se reposer. Malheureusement il n’y en a pas, et pour cause : la méthode SaveClient fait appel aux méthodes SrvOffreCommerciale.MajTunnelOffreCommerciale, QryTunnel.SearchContractIdUsingSameEmail, CreateIdentifiant et ReturnClientOk.

On ne peut pas le deviner, mais derrière ces méthodes se cachent des centaines de lignes de code qui exécutent des requêtes en base de données, des appels HTTP, et beaucoup de logique métier. Tester la méthode SaveClient en l’état est problématique car :

  • Cela nécessite un environnement complexe : une base de données de tests contenant les informations requises, des services HTTP fonctionnels, alors qu’un test unitaire doit s’exécuter de façon isolée de l’infrastructure.
  • L’exécution du test durerait probablement plusieurs secondes (car les appels en base et les requêtes HTTP rallongent sensiblement son exécution), et un test unitaire doit s’exécuter très rapidement (si possible en quelques dizaines voire centaines de millisecondes) car il est censé être lancé régulièrement par le développeur afin de valider son travail.
  • Le test vérifierait beaucoup trop de choses car le code derrière les méthodes MajTunnelOffreCommerciale, SearchContractIdUsingSameEmail, CreateIdentifiant et ReturnClientOk serait exécuté systématiquement, et un bug dans l’une d’elles entraînerait l’échec du test. Or, le rôle d’un test unitaire est de ne tester qu’une seule chose (le code de la méthode SaveClient en l’occurrence).

Les méthodes MajTunnelOffreCommerciale, SearchContractIdUsingSameEmail, CreateIdentifiant et ReturnClientOk et les classes SrvOffreCommerciale, QryTunnel et SrvTunnel où elles sont déclarées sont des dépendances fortes de la méthode SaveClient. Pour rendre cette dernière testable, on peut essayer d’abstraire ces dépendances en introduisant des interfaces.

Voici un exemple de refactoring de ce code qui permettrait de tester unitairement la méthode SaveClient :

Cette nouvelle version a nécessité les changements suivants :

  • Les appels aux méthodes statiques MajTunnelOffreCommerciale, SearchContractIdUsingSameEmail, CreateIdentifiant et ReturnClientOk ont été remplacés par des appels à des méthodes d’instance.
  • Il a fallu créer quatre nouvelles interfaces : ISrvTunnelFactory, ISrvTunnel, ISrvOffreCommerciale et IQryTunnel.
  • La classe TunnelController n’est plus statique, son constructeur attend trois paramètres : le premier de type ISrvTunnelFactory, le second ISrvOffreCommerciale et le dernier IQryTunnel. Les dépendances de la méthode SaveClient sont donc à présent injectées via le constructeur, et le fait qu’elles soient des interfaces permettra d’écrire des tests unitaires où elles pourront être mockées.

Les nouvelles interfaces doivent bien sûr être implémentées, chaque implémentation étant dans un premier temps un simple proxy où on retrouve l’appel de méthode statique qui était précédemment dans la méthode SaveClient :

Malgré tout, le fait de modifier le caractère statique de la classe TunnelController et sa méthode SaveClient peut être problématique dans le sens où cela change la façon dont le code appelant interagit avec elles. Lorsque la méthode était statique, on écrivait :

Dorénavant il faut une instance de la classe TunnelController pour exécuter sa méthode SaveClient, et pour cela on a besoin des trois paramètres à passer à son constructeur :

Un framework d’injection de dépendances comme Simple Injector, Autofac, Unity ou un autre nous épargnera les instanciations manuelles de ces classes, mais cela ne change rien au fait que le code appelant notre méthode doit être modifié. Si cette méthode est utilisée à de nombreux endroits dans l’application on devra modifier beaucoup plus de code que ce qu’on avait imaginé. C’est ce qui s’appelle tirer le fil de la pelote.

Cette situation désagréable peut vite décourager un développeur guidé par les meilleures intentions. Pourtant il est possible de revoir le design pour que notre méthode soit testable tout en restant statique (et donc inchangée du point de vue de ses appelants) : en utilisant un Service Locator.

Ce pattern n’est pas le plus élégant (on lui préfère souvent celui d’injection de dépendances) mais il est très utile pour régler notre problème car il permet d’obtenir de façon statique l’implémentation d’une interface qui a été préalablement enregistrée (généralement au démarrage de l’application ou du test unitaire).

Dans notre cas, nous utilisons le package NuGet CommonServiceLocator où est définie la classe statique ServiceLocator qui nous servira à récupérer les objets dont nous avons besoin. La nouvelle implémentation ressemble à cela :

Aux lignes 11, 16 et 19, la méthode SaveClient récupère de façon statique les instances des interfaces ISrvOffreCommerciale, IQryTunnel et ISrvTunnelFactory à l’aide du service locator.

Pour que cette méthode fonctionne, il faut configurer le service locator et définir les associations entre les interfaces et leurs implémentations, ce que nous avons fait à l’aide du conteneur d’injection de dépendances Unity. Le code suivant illustre cette configuration, il doit être placé de façon à s’exécuter au démarrage de l’application :

Ce bout de code à placer au démarrage de l’application enregistre dans le service locator les implémentations des interfaces ISrvOffreCommercial, IQryTunnel, ISrvTunnelFactory.

Maintenant que les dépendances de notre méthode ont été abstraites, il est possible de leur substituer des mocks et de tester unitairement notre code. Les mocks sont créés à l’aide des classes du package NuGet NSubstitute et enregistrés dans le service locator lors de la phase de configuration du test (dans la méthode SetUp, nous utilisons ici le framework de test NUnit) :

Dans la méthode SetUp, les interfaces ISrvOffreCommerciale, IQryTunnel et ISrvTunnelFactory sont enregistrées dans le service locator et associées à des mocks.

A présent, il suffit de rajouter d’autres tests unitaires vérifiant les propriétés de la méthode pour disposer d’un moyen rapide de détecter les éventuelles régressions lors des modifications futures. Cette batterie de tests constitue une sécurité pour le développeur lors des refactorings : si des tests échouent à la suite d’une modification de code, il saura rapidement que ses changements ont cassé une fonctionnalité et pourra corriger le problème au plus tôt. Au contraire, si tous les tests sont au vert, ils lui assurent que les fonctionnalités testées se comportent toujours comme prévu.

Nous avons réussi à rendre la classe testable, mais ce n’est que la première étape de notre réappropriation de ce code legacy. Maintenant que nous disposons du filet de sécurité que sont les tests unitaires, nous allons pouvoir travailler notre méthode plus sereinement, que ce soit pour lui ajouter des fonctionnalités ou bien la refactorer petit à petit.

Une modification intéressante pourrait être de supprimer le caractère statique de notre méthode et de sa classe et d’utiliser le pattern d’injection de dépendances plutôt qu’un service locator pour transmettre les services ISrvOffreCommercial, IQryTunnel et ISrvTunnelFactory. La méthode statique étant appelée à de nombreux endroits de l’application, la supprimer serait trop brutal et obligerait à modifier chacun de ces endroits pour que l’application compile à nouveau. Le mieux est d’exposer une nouvelle version non statique de la méthode tout en conservant la méthode statique : cela permettra de migrer progressivement les appels statiques vers la nouvelle méthode d’instance.

Il faut d’abord transformer la classe de la façon suivante :

  • Supprimer le mot-clé static de la définition de la classe TunnelController.
  • Supprimer le mot-clé static de la déclaration de la méthode SaveClient.
  • Ajouter un constructeur à la classe TunnelController avec trois paramètres : ISrvOffreCommerciale, IQryTunnel, ISrvTunnelFactory. Ces paramètres représentent les dépendances de la classe et seront assignés à trois nouveaux attributs.
  • Ajouter une méthode d’instance à TunnelController : SaveClient2, qui reprend la logique de la méthode statique SaveClient (marquée à présent comme obsolète) mais qui utilise les attributs de la classe en remplacement des variables obtenues à l’aide du service locator.

La suite se passe dans la méthode SaveClient statique, qu’on retravaille pour que sa nouvelle logique consiste à :

  1. Obtenir trois variables de type ISrvOffreCommerciale, IQryTunnel et ISrvTunnelFactory à l’aide du service locator.
  2. Créer une instance de TunnelController (sa classe parente).
  3. Appeler la méthode SaveClient2 de l’instance de TunnelController créée précédemment et retourner son résultat.

Dans cette nouvelle version, les dépendances de la classe sont clairement visibles (ce sont les paramètres de son constructeur) et non plus cachées dans une méthode sous la forme d’appels au service locator. La méthode statique SaveClient a toujours le même comportement et s’utilise toujours de la même façon, mais on peut maintenant commencer à modifier ses appelants l’un après l’autre pour les faire utiliser la nouvelle méthode non statique SaveClient2. Lorsque que tous les appels à la méthode SaveClient auront été remplacés, cette méthode sera devenue du code mort et pourra être supprimée, mettant fin à la même occasion à la dépendance de la classe au service locator.

Il est important d’aller jusqu’au bout de cette migration et de ne pas laisser le code dans un état intermédiaire où coexisteraient les deux méthodes. Les refactorings à moitié terminés sont pires que ceux qui n’ont jamais commencé : ils créent du bruit supplémentaire dans le code et brouillent encore plus sa compréhension par les développeurs qui en hériteront.


En résumé

Une application legacy est rarement un cadeau, sa maintenance peut rapidement se révéler être un cauchemar et user les développeurs. Pourtant les quelques techniques simples présentées ici peuvent sensiblement améliorer cette situation.

Le principe consiste en fait à se rendre service en arrêtant de se faire du mal. En automatisant les tâches sans valeur ajoutée, longues, répétitives ou encore sujettes à erreurs comme peut l’être le déploiement manuel de l’application, on fiabilise nos livraisons tout en gagnant du temps pour se consacrer aux travaux où on est réellement utile (la conception de logiciels, pardi !). En instrumentant ses applications pour mettre au jour les bugs, on ouvre la boîte noire et on perd moins de temps lors des phases de diagnostic d’erreurs. En réorganisant notre code, on rend possible sa sécurisation avec des tests unitaires sur lesquels s’appuyer pour améliorer le design de l’application, mieux la maîtriser et donc en faciliter la maintenance et l’évolution.

Si vous avez des astuces à partager pour vous simplifier la vie dans ce genre de contextes, n’hésitez pas à les partager, ça m’intéresse grave !

YounitedTech

Le blog Tech de Younited, où l’on parle de développement, d’architecture, de microservices, de cloud, de data… Et de comment on s’organise pour faire tout ça. Ah, et on recrute aussi, on vous a dit ?

François Hyvrier

Written by

YounitedTech

Le blog Tech de Younited, où l’on parle de développement, d’architecture, de microservices, de cloud, de data… Et de comment on s’organise pour faire tout ça. Ah, et on recrute aussi, on vous a dit ?

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade