Mesurer la testabilité à l'aide d'une métrique
Je me permet de réagir au billet d'Olivier Hoareau à propos de l'injectabilité/mockabilité d'un code source comme indicateur de la testabilité dudit code.
Olivier préconise d'utiliser la formule suivante pour mesurer la testabilité d'un code, arguant du fait que plus un code est modulaire et découplé, plus il est testable :
i = (nb getters + nb setters) / (2 * nb propriétés) avec 0 <= i <= 1
Si je suis d'accord avec le fait qu'un code découplé et injectable est plus facilement et efficacement testable, je trouve que sa formule insuffisante et trop restrictive, car elle ne prend pas en compte l'encapsulation.
En effet, si elle fonctionne pour tout ce qui est propriété publique d'une classe, elle ne prend pas du tout en compte les propriétés protégées ou privées.
De par leur nature même, ce type de propriété ne devrait pas pouvoir être modifiée par le programmeur.
Ainsi, si une méthode publique pour récupèrer la valeur d'une propriété protégée ou privée est tout à fait tolérable, une méthode publique pour modifier une telle propriété est une hérésie absolue en programmation orientée objet.
En effet, à quoi bon définir une propriété protégée ou privée si le développeur peut en faire ce qu'il veut quand il veut ?
Pourtant, il faut bien pouvoir tester les classes qui ont des propriétés de ce type.
Pour ce faire, il y a donc au moins deux solutions :
La première, que j'appelerai l'injection de dépendance statique, est de passer en argument au constructeur de la classe les objets qui devront être utilisés pour initialiser ses propriétés protégées ou privées.
L'objet est donc construit avec les objets que désire le programmeur, sans que ce dernier puisse par la suite modifier ces derniers.
L'utilisation d'une valeur par défaut pour ces arguments permet de s'affranchir des arguments dédiés aux tests dans le code de production et permet de garder un code lisible.
Le code ressemble alors à cela :
class myClass
{
private $db = null;
public $public = '';
public function __construct($public, db $db = null)
{
$this->public = $public;
if ($db === null)
{
$db = new defaultDb();
}
$this->db = $db;
}
}
Le code est alors complétement testable, sans aucune méthode permettant de définir ou modifier les propriétés privée ou protégées de la classe.
Il suffit d'appeler dans les tests le constructeur de l'objet avec une instance de la classe de son choix pour définir db :
class testMyClass extends test
{
public function test__construct()
{
...
$myClass = new myClass(uniqid(), new testDb());
...
}
}
La seconde solution, plus lourde et que je n'utilise pas, consiste à utiliser l'injection de dépendance dynamique, telle que celle présentée par Fabien Potencier.
Il est alors possible d'injecter les objets de son choix dans les propriétés protégées ou privées d'une classe sans passer par des arguments dédiés aux tests.
Le code est donc plus "propre", mais il est plus lourd et plus gourmand en ressource.
En effet, comme d'habitude, ce que le développeur gagne en facilité de codage, la machine le perd en performance.
Et pour le coup, même si je suis convaincu que la machine doit travailler au maximum pour l'humain et non le contraire, je trouve que la surchage demandée par ce type de traitement par rapport au confort gagné par le programmeur ne vaut pas le coup.
Mais cela, est une autre histoire qui n'est pas l'objet de ce billet.
En effet, la formule d'Olivier ne prend pas en compte ces techniques qui ne reposent pas sur des accesseurs ou des modificateurs déclarés explicitement par le programmeur, mais sur des méthodes "magiques" et implicites, invisible au niveau de l'interface publique de la classe.
Il n'en reste cependant pas moins que sa formule, même imparfaite, est une bonne approche pour mesurer la testabilité d'un code et qu'il serait intéressant de trouver un moyen pour qu'elle prenne en compte l'injection de dépendance, statique ou dynamique, au niveau des membres protégés et privés d'une classe.

Commentaires
Merci pour ces précisions qui poussent la réflexion un cran plus haut
En effet la formule est loin d'être parfaite car il s'agit d'un premier jet que j'étalonne actuellement sur du code en masse, comme toutes métriques elle a ses limitations, mais concrètement, elle fait ressortir les fichiers/classes sur lesquels il y a "quelque chose à dire". Finalement, ce n'est pas la valeur en elle même qui est importante mais l'appréciation, la différence relative avec les autres. Sur la dizaine de projet sur laquelle je l'ai expérimentée, elle m'a permis de mettre en exergue, quasi systématiquement les classes "dangereuses" (difficilement testable, compliquée...), et c'est ça le but de la manoeuvre surtout quand il y a des centaines de classes à vérifier...
L'injection de dépendances avec des méthodes/techniques magiques me semble dangereuse en contexte d'équipe car, maîtrisé par celui qui la développe, elle ne le sera pas forcément par les autres développeurs (i.e. "ce n'est pas standard"), où ceux qui arrivent après la bataille
Je suis d'accord avec le fait que l'utilisation de méthodes publiques (setters/getters) n'est pas la plus adaptée pour certaines propriétés qu'il est souhaitable de garder en private/protected. Cependant toute variable de classe en private / protected est un "état" de l'objet (l'instance). 2 appels successifs à une méthode de l'objet peuvent donc se comporter différemment en fonction de l'état de l'objet. Le fait de positionner via un setter la valeur d'une variable de la classe et d'appeler directement une méthode de la classe permet de tester unitairement cette méthode dans un contexte "connu" (i.e. "je sais que la valeur de la variable est X " avant d'appeler la méthode Y).
Sans les getters/setters le code peut être testable mais, à mon avis, pas toujours facilement testable "unitairement".
D'autre part, PHP est un langage assez permissif. Cela permet de faire de belles choses (des fois même de faire sa petite grenouille qui parle à soi...) mais cela permet de faire des choses compliquées / inmaintenables / inutiles aussi. La programmation objet pur et stricte telle que l'on nous l'apprend à l'école, personnellement je la trouve assez inadaptée à du code simple / maintenable / évolutif, bien sûr on peut faire des méthodes, propriétés et classes privées en POO, mais pour les tester unitairement après c'est une autre histoire (surtout quand on a une grappe d'objet encapsulés dans une variable d'une classe et que l'on ne peut pas instancier comme on veut ces objets via des setters). Je suis conscient de cette difficulté, et personnellement, j'ai fais le choix de développer plutôt "compatible" tests / maintenabilité / évolutivité et surtout, surtout "simplicité", même si je n'y arrive bien sûr pas toujours.
En tout cas merci pour ces précisions.
Je pense que nous aurons l'occasion de discuter d'autres "formules" bientôt...
@olivier:
Tester des méthodes protégées ou privées n'a strictement aucun sens, puisqu'elle ne sont pas publiques.
En effet, leur seule raison d'exister est d'être utilisée soit dans d'autre méthode privées ou protégées, soit dans des méthodes publiques pour participer à l'obtention du résultat de la méthode.
Elles sont donc de par leur nature même testées transitivement, via les tests sur les méthodes publiques.
Bref, vouloir tester des méthodes de ce type ne rime à rien, et par conséquent, il est inutile de chercher à bypasser le mécanisme d'encapsulation par l'utilisation d'accesseur ou de modificateur, même dans le cadre précis des tests.
L'encapsulation est un mécanisme qui a sa raison d'exister, ne serait ce que pour éviter la corruption des objets par le programmeur au cours de leur vie, ou bien justement pour le travail en équipe, puisqu'elle permet au programmeur de ne pas faire n'importe quoi.
L'encapsulation permet donc à un développeur qui ne connait pas le fonctionnement interne de l'objet (et il ne faut pas oublier que l'un des buts de POO est de masquer l'implémentation entre autre à l'aide de l'encapsulation) de l'utiliser sans compromettre son intégrité et éventuellement rendre instable le code dans son ensemble.
Et pour initialiser un contexte d'éxécution précis dans le cadre de tests sans accesseur/modificateur sur des membres privés ou protégés, il suffit de créer une instance de la classe à tester pour chaque contexte, en lui passant les arguments qui correspondent à ce contexte précis lors de sa construction.
C'est bien sur un peu plus verbeux, mais tellement plus propre !