Un test peut en cacher un autre – Un peu de théorie

Introduction

En discutant avec des développeurs, je remarque plusieurs choses :

  • Nos approches sur l’architecture applicative du code sont différentes
  • Les définitions que nous donnons aux catégories de tests sont différentes
  • Les façons de rédiger les tests sont différentes

Sans généraliser, je pense qu’il est parfois difficile dans ce contexte d’identifier précisément quoi tester et comment.

Être au clair sur ces trois points me semble important et permet de me faciliter la vie et d’être plus confiant au quotidien, laissant beaucoup moins de place à l’ambiguïté sur des questions comme :

  • Est-ce que je dois vérifier cette problématique comme ça ?
  • Est-ce que je ne vérifie pas trop de choses ? (Par exemple : les règles métiers plus les interactions avec la base de données en même temps)

Les grandes approches TDD

Les premières questions que l’on se pose lorsque l’on débute par les tests, c’est : “Mais par où vais-je commencer ? Quel sera mon premier test ? Avec quelle approche vais-je résoudre ce problème ?”

Est-ce que le meilleur départ est de commencer par les détails pour laisser grandir l’architecture au fur et à mesure ?
Ou, au contraire, débuter avec une big picture et laisser les détails se révéler d’eux-mêmes ?

Il existe deux grandes approches :

Inside Out ou Chicago School ou Approche Classique

Cette approche vise à s’assurer des dénouements précis et non de la collaboration entre les objets. Ainsi, elle a tendance à partir de l’intérieur vers l’extérieur.
C’est une pratique qui emploie un très faible usage de mocks pour se suffire des stubs. Pour avoir plus de précisions sur les mocks et stubs : https://martinfowler.com/bliki/TestDouble.html.

Outside In ou London School ou Approche Mockiste

Cette approche vise à s’assurer d’une collaboration précise entre les objets.
Elle a donc tendance à promouvoir l’usage de mocks, et ainsi de démarrer depuis l’extérieur (REST API/swing, etc.) vers l’intérieur.

Pour aller plus loin sur ces deux approches :

Malheureusement, elles ne permettent pas de répondre ou très indirectement à la question : Comment je peux développer et vérifier efficacement la solution à un problème donné (une règle métier, une interaction avec une base de donnée, etc.) ?


Les fondements d’une nouvelle approche

Pour appréhender au mieux l’approche que je vais proposer, il est préférable d’avoir deux pré requis.

Le principe d’inversion des dépendances pour se recentrer sur la valeur métier et ses règles

Ce dernier est au centre de plusieurs architectures.

Permettre à une application d’être pilotée aussi bien par des utilisateurs que par des programmes, des tests automatisés ou des scripts batchs, et d’être développée et testée en isolation de ses éventuels systèmes d’exécution et bases de données.
– Alistair Cockburn (Hexagonal Architecture ou Ports/Adapters Architecture)

 

L’élément clé d’une application ne réside pas dans sa base de données et les frameworks utilisés.
Les use-cases d’une application sont l’élément central.
– Robert C. Martin (Clean Architecture)

Pour explorer davantage les concepts et le lexique de ces architectures, je vous conseille ces articles :

Dans la suite, j’utiliserai le schéma ci-dessous pour mettre en avant les différentes parties utilisées :

Les flèches représentent le sens des dépendances, ce sont les côtés Adapters qui dépendent du centre Core qui détient les règles métiers et non l’inverse.

 

L’approche de Ian Cooper sur le TDD

Pour lui un gros problème porte sur l’interprétation des gens de la propriété unitaire d’un test. En effet, un test unitaire doit pouvoir se lancer en isolation des autres, ce qui est résumé par : “si je crée cette classe alors elle doit être testée toute seule pour respecter la propriété”.

Cette vision nous éloigne d’une approche basée sur le comportement et c’est plutôt cela que l’on souhaite vérifier. C’est bien un test sur un comportement qui doit être isolé d’un autre.

Un autre problème, les développeurs/ses testent leurs détails d’implémentation. Du coup, ils écrivent beaucoup de tests, ce qui ralentit le développement et quand ils refactorent (activité qui consiste à changer l’implémentation sans changer le comportement d’un code) beaucoup de tests cassent. Cela engendre des abandons de TDD.

Alors qu’écrire ses tests via l’API publique de son code réduit le nombre de tests et rend plus productif, de plus le code devient plus simple à refactorer.

Pour résumer ces deux points donnent les avantages suivants :

  • Réduction du nombre de tests
  • Amélioration du refactoring

Enfin, il recommande d’utiliser des architectures permettant d’isoler son domaine des frameworks ou autres technologies (exemple : accès à la base de données), comme l’architecture Ports/Adapters.

Conférence TDD, where did it all go wrong

Article : http://codebetter.com/iancooper/2011/10/06/avoid-testing-implementation-details-test-behaviours/

On peut extraire deux points d’attention qui se suivent dans l’article ci-dessus :

  1. Le code issu d’un refactoring ne requiert pas de faire de nouveaux tests ! Car ce dernier est déjà couvert par les tests qui ont amené à la première implémentation, on a juste nettoyé et rangé proprement le code.
  2. La compréhension du point précédent, démontre qu’une approche “un fichier de tests par classe/fichier de production” n’est pas une bonne approche pour saisir l’essence de TDD. Ajouter une nouvelle classe ne déclenche pas forcément l’écriture de tests. Je vous recommande d’utiliser ports/adaptateurs et d’écrire les tests en outside-in (cf. vu au début) depuis le use case.

L’application théorique

Si nous reprenons la pyramide des tests, on peut distinguer quatre grandes catégories :

  • Tests unitaires
  • Tests d’acceptation/fonctionnels
  • Tests d’intégration
  • Tests de bout en bout

La première catégorie me semble essentielle. Elle permet d’avoir une boucle de feedback des plus rapides sur le développement en cours et reste relativement simple à mettre en place.

Maintenant, mettons en évidence les points d’entrée et les parties vérifiées par chacune des catégories.

Tests unitaires

Les points d’entrée sont les Use Cases de l’application et ces tests s’arrêtent aux frontières du Core avec l’utilisation d’une fausse implémentation d’un Adapter de droite (cf. les articles pour connaître leurs particularités). 

Ils se focalisent sur la vérification de la bonne implémentation des règles métiers et de ce qui a de la valeur pour l’entreprise.

Tests d’acceptation/fonctionnels

Les points d’entrée sont les Use Cases de l’application et ces tests s’arrêtent aux frontières du Core avec l’utilisation d’une fausse implémentation d’un Adapter de droite (cf. l’article pour connaître leurs particularités). 

Ils se focalisent sur la vérification de la bonne implémentation des règles métiers et de ce qui a de la valeur pour l’entreprise.

La nuance avec les tests unitaires est qu’ils sont plus macros et émergent différemment. Nous verrons un cas d’usage dans un article dédié.

Tests d’intégration

Les points d’entrée sont les Adapters de l’application et ces tests s’arrêtent à cette case. 

Ils se focalisent sur la vérification de la bonne intégration avec un système tiers (ex. une base de données, une REST API, l’affichage des données pour un client, etc.), sans se préoccuper des règles métiers. 

On fera en général un ou quelques cas passants et non-passants, si ces derniers existent.

Exemples :

  • D’un cas passant : Bonne réception de données venant d’une base de données
  • D’un cas non-passant : Que se passe-t-il quand la REST API nous informe que les données envoyées sont incorrectes (403 – Bad Request) ?

Tests de bout en bout

Les points d’entrée sont les Adapters de gauche de l’application (cf. l’article pour connaître leurs particularités) et ces tests, orientés fonctionnels, ont pour but de traverser toutes les couches de l’application ; Adapters et Core en étant ou non connecté aux systèmes tiers (base de données, REST API, etc.).

Ils se focalisent sur la vérification du bon fonctionnement de l’ensemble du projet. On pourra par exemple faire un chemin critique de celui-ci.


Conclusion

Nous avons vu qu’il y avait deux grandes approches, mais elles ne répondent pas à certaines interrogations et laissent des délimitations floues de l’application.

En conjuguant, l’approche de Ian Cooper avec le principe d’inversion des dépendances porté par certaines architectures (Clean Architecture, Hexagonale Architecture), nous pouvons avoir des associations plus claires :

  • Tests unitaires et tests d’acceptation : règles métiers
  • Test d’intégration : intégration avec un système tiers
  • Test de bout en bout : bonne collaboration de l’ensemble du système

Annexe : https://www.stevefenton.co.uk/2013/05/my-unit-testing-epiphany/

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *


Ce formulaire est protégé par Google Recaptcha