Dans ce tutoriel, je présenterai un exemple de bout en bout d’une application simple, réalisée strictement avec TDD en PHP. Je vous guiderai pas à pas, une à la fois, tout en vous expliquant les décisions que j'ai prises afin de mener à bien cette tâche. L'exemple suit de près les règles de TDD: écrire des tests, écrire du code, refactor.
TDD est une technique "test-first" pour développer et concevoir des logiciels. Il est presque toujours utilisé dans les équipes agiles, étant l’un des outils essentiels du développement logiciel agile. Le TDD a été défini et présenté au monde professionnel par Kent Beck en 2002. Depuis, il est devenu une technique acceptée - et recommandée - dans la programmation quotidienne..
TDD a trois règles de base:
PHPUnit est l'outil qui permet aux programmeurs PHP d'effectuer des tests unitaires et de pratiquer le développement piloté par les tests. Il s’agit d’un framework de test unitaire complet avec un support moqueur. Même s'il existe quelques alternatives, PHPUnit est la solution la plus utilisée et la plus complète pour PHP aujourd'hui..
Pour installer PHPUnit, vous pouvez suivre le didacticiel précédent dans notre session "TDD en PHP" ou utiliser PEAR, comme expliqué dans la documentation officielle:
racine
Ou utiliser sudo
mise à niveau de poire PEAR
pear config-set auto_discover 1
pear installer pear.phpunit.de/PHPUnit
Vous trouverez plus d'informations et d'instructions sur l'installation de modules PHPUnit supplémentaires dans la documentation officielle..
Certaines distributions Linux offrent phpunit en tant que paquet précompilé, bien que je recommande toujours une installation, via PEAR, car il garantit que la version la plus récente et la plus récente est installée et utilisée.
Si vous êtes un fan de NetBeans, vous pouvez le configurer pour qu'il fonctionne avec PHPUnit en procédant comme suit:
Si vous n'utilisez pas d'IDE avec une prise en charge des tests unitaires, vous pouvez toujours exécuter votre test directement à partir de la console:
cd / mon / applications / test / dossier phpunit
Notre équipe est chargée de la mise en œuvre d'une fonctionnalité de "Word Wrap".
Supposons que nous faisons partie d'une grande société, qui a une application sophistiquée à développer et à maintenir. Notre équipe est chargée de la mise en œuvre d'une fonctionnalité de «retour à la ligne». Nos clients ne souhaitent pas voir les barres de défilement horizontales, et il est difficile de se conformer.
Dans ce cas, nous devons créer une classe capable de formater un bit de texte arbitraire fourni en entrée. Le résultat devrait être mot enveloppé à un nombre spécifié de caractères. Les règles d'habillage de mots doivent suivre le comportement des autres applications quotidiennes, telles que les éditeurs de texte, les zones de texte de page Web, etc. Notre client ne comprend pas toutes les règles d'habillage de mots, mais il sait qu'il le souhaite et le sait devrait fonctionner de la même manière qu'ils ont connu dans d'autres applications.
TDD vous aide à obtenir une meilleure conception, mais n'élimine pas le besoin d'une conception et d'une réflexion initiales..
Une des choses que beaucoup de programmeurs oublient, après avoir démarré TDD, est de penser et de planifier à l’avance. TDD vous aide à obtenir une meilleure conception la plupart du temps, avec moins de code et de fonctionnalités vérifiées, mais n'élimine pas le besoin d'une conception et d'une pensée humaine initiales..
Chaque fois que vous avez besoin de résoudre un problème, vous devez prévoir du temps pour y réfléchir, pour imaginer un petit dessin - rien d'extraordinaire - mais suffisamment pour vous aider à démarrer. Cette partie du travail vous aide également à imaginer et à deviner des scénarios possibles pour la logique de l'application..
Pensons aux règles de base pour une fonctionnalité de retour à la ligne. Je suppose qu'un texte non enveloppé nous sera donné. Nous connaîtrons le nombre de caractères par ligne et nous voudrons que celle-ci soit enveloppée. Donc, la première chose qui me vient à l’esprit est que, si le texte contient plus de caractères que le nombre sur une ligne, nous devrions ajouter une nouvelle ligne au lieu du dernier caractère d’espace restant sur la ligne..
D'accord, cela résumerait le comportement du système, mais c'est beaucoup trop compliqué pour tout test. Par exemple, qu’en est-il quand un seul mot est plus long que le nombre de caractères autorisés sur une ligne? Hmmm… cela ressemble à un cas de bord; nous ne pouvons pas remplacer un espace par une nouvelle ligne car nous n'avons pas d'espace sur cette ligne. Nous devrions forcer envelopper le mot, en le séparant efficacement en deux.
Ces idées devraient être suffisamment claires pour que nous puissions commencer à programmer. Nous aurons besoin d'un projet et d'une classe. Appelons ça Wrapper
.
Créons notre projet. Il devrait y avoir un dossier principal pour les classes source et un Tests /
dossier, naturellement, pour les tests.
Le premier fichier que nous allons créer est un test dans le Des tests
dossier. Tous nos futurs tests seront contenus dans ce dossier, je ne le spécifierai donc plus explicitement dans ce tutoriel. Nommez la classe de test quelque chose de descriptif, mais simple. WrapperTest
va faire pour le moment; notre premier test ressemble à ceci:
require_once dirname (__ FILE__). '/… /Wrapper.php'; La classe WrapperTest étend PHPUnit_Framework_TestCase function testCanCreateAWrapper () $ wrapper = new Wrapper ();
Rappelles toi! Nous ne sommes pas autorisés à écrire un code de production avant un test ayant échoué - pas même une déclaration de classe! C'est pourquoi j'ai écrit le premier test simple ci-dessus, appelé canCreateAWrapper
. Certains considèrent cette étape comme inutile, mais je considère que c'est une belle occasion de réfléchir au cours que nous allons créer. Avons-nous besoin d'un cours? Comment devrions-nous l'appeler? Devrait-il être statique?
Lorsque vous exécutez le test ci-dessus, vous recevez un message d'erreur irrécupérable du type suivant:
Erreur irrécupérable PHP: require_once (): Echec de l'ouverture requise '/ chemin / vers / WordWrapPHP / Tests /… /Wrapper.php' (include_path = '.: / Usr / share / php5: / usr / share / php') dans / chemin / vers / WordWrapPHP / Tests / WrapperTest.php on line 3
Beurk! Nous devrions faire quelque chose à ce sujet. Créer un vide Wrapper
classe dans le dossier principal du projet.
classe Wrapper
C'est tout. Si vous relancez le test, il réussit. Félicitations pour votre premier test!
Nous avons donc notre projet mis en place et en cours d'exécution; maintenant, nous devons penser à notre premier réal tester.
Quel serait le plus simple… le plus bête… le test le plus fondamental qui ferait échouer notre code de production actuel? Eh bien, la première chose qui me vient à l’esprit est "Donnez un mot assez court et attendez-vous à ce que le résultat soit inchangé."Cela semble faisable; écrivons le test.
require_once dirname (__ FILE__). '/… /Wrapper.php'; La classe WrapperTest étend PHPUnit_Framework_TestCase function testDoesNotWrapAShorterThanMaxCharsWord () $ wrapper = new Wrapper (); assertEquals ('word', $ wrapper-> wrap ('word', 5));
Cela semble assez compliqué. Que signifie "MaxChars" dans le nom de la fonction? Qu'est-ce que 5
dans le emballage
méthode se référer à?
Je pense que quelque chose n'est pas tout à fait juste ici. N'y a-t-il pas un test plus simple que nous pouvons exécuter? Oui, certainement! Et si nous enroulions… rien - une ficelle vide? Ça sonne bien. Supprimez le test compliqué ci-dessus et ajoutez plutôt notre nouveau test plus simple, présenté ci-dessous:
require_once dirname (__ FILE__). '/… /Wrapper.php'; class WrapperTest étend PHPUnit_Framework_TestCase function testItShouldWrapAnEmptyString () $ wrapper = new Wrapper (); $ this-> assertEquals (", $ wrapper-> wrap ("));
Ceci est vraiment mieux. Le nom du test est facile à comprendre, nous n’avons pas de chaînes ni de chiffres magiques, et surtout, CELA FAIL!
Erreur fatale: appel de la méthode non définie Wrapper :: wrap () dans…
Comme vous pouvez le constater, j'ai supprimé notre tout premier test. Il est inutile de vérifier explicitement si un objet peut être initialisé, alors que d'autres tests en ont également besoin. C'est normal. Avec le temps, vous constaterez que la suppression de tests est chose courante. Les tests, en particulier les tests unitaires, doivent être rapides - très rapides… et fréquemment - très fréquemment. Compte tenu de cela, il est important d’éliminer la redondance dans les tests. Imaginez que vous exécutiez des milliers de tests chaque fois que vous enregistrez le projet. Cela ne devrait pas prendre plus de deux minutes, maximum, pour qu’ils courent. Donc, ne soyez pas terrifié pour supprimer un test, si nécessaire.
Pour en revenir à notre code de production, passons ce test:
class Wrapper function wrap ($ text) return;
Ci-dessus, nous n’avons ajouté absolument pas plus de code que nécessaire pour réussir le test..
Maintenant, pour le prochain test ayant échoué:
fonction testItDoesNotWrapAShortEnoughWord () $ wrapper = new Wrapper (); $ this-> assertEquals ('mot', $ wrapper-> wrap ('mot', 5));
Message d'échec:
Échec de l'affirmation que null correspond au «mot» attendu.
Et le code qui le fait passer:
function wrap ($ text) return $ text;
Hou la la! C'était facile, n'est-ce pas?
Pendant que nous sommes dans le vert, notez que notre code de test peut commencer à pourrir. Nous devons revoir certaines choses. Rappelez-vous: toujours refactoriser lorsque vos tests sont réussis; c'est la seule façon pour vous de vous assurer que vous avez correctement refactored.
Tout d'abord, supprimons la duplication de l'initialisation de l'objet wrapper. Nous ne pouvons le faire qu’une fois dans le installer()
méthode, et l'utiliser pour les deux tests.
class WrapperTest étend PHPUnit_Framework_TestCase private $ wrapper; fonction setUp () $ this-> wrapper = new Wrapper (); function testItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap (")); function testItDoesNotWrapAShortEnoughWord () $ this-> assertEquals ('mot', $ this-> wrapper-> wrap ('mot', 5));
le
installer
la méthode fonctionnera avant chaque nouveau test.
Ensuite, il y a des bits ambigus dans le deuxième test. Qu'est-ce que le mot? Qu'est-ce que '5'? Soyons clairs pour que le prochain programmeur qui lit ces tests n'ait pas à deviner.
N'oubliez jamais que vos tests constituent également la documentation la plus récente pour votre code..Un autre programmeur devrait pouvoir lire les tests aussi facilement qu’il lirait la documentation..
fonction testItDoesNotWrapAShortEnoughWord () $ textToBeParsed = 'word'; $ maxLineLength = 5; $ this-> assertEquals ($ textToBeParsed, $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Maintenant, relisez cette affirmation. Cela ne lit-il pas mieux? Bien sûr que si. N'ayez pas peur des noms de variable trop longs pour vos tests; l'auto-complétion est votre ami! Il vaut mieux être le plus descriptif possible.
Maintenant, pour le prochain test ayant échoué:
function testItWrapsAWordLongerThanLineLength () $ textToBeParsed = 'alongword'; $ maxLineLength = 5; $ this-> assertEquals ("le long de \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Et le code qui le fait passer:
function wrap ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) renvoie substr ($ text, 0, $ lineLength). "\ n". substr ($ text, $ lineLength); return $ text;
C'est le code évident pour rendre notre dernier passer un test. Mais attention, c’est aussi le code qui fait notre premier test échoué!
Nous avons deux options pour résoudre ce problème:
Si vous choisissez la première option, rendant le paramètre facultatif, vous rencontrerez un petit problème avec le code actuel. Un paramètre facultatif est également initialisé avec une valeur par défaut. Quelle pourrait être une telle valeur? Zéro peut sembler logique, mais cela impliquerait d'écrire du code uniquement pour traiter ce cas particulier. Définir un très grand nombre, de sorte que le premier si déclaration ne donnerait pas vrai peut être une autre solution. Mais quel est ce nombre? Est-ce 10? Est-ce 10000? Est-ce 10000000? On ne peut pas vraiment dire.
Compte tenu de tout cela, je vais juste modifier le premier test:
fonction testItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap (", 0));
Encore une fois, tout vert. Nous pouvons maintenant passer au test suivant. Faisons en sorte que, si nous avons un très long mot, il se termine sur plusieurs lignes.
fonction testItWrapsAWordSeveralTimesIfItsTooLong () $ textToBeParsed = 'averyverylongword'; $ maxLineLength = 5; $ this-> assertEquals ("avery \ nveryl \ nongwo \ nrd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Cela échoue évidemment, parce que notre code de production réel ne s'enroule qu'une fois.
Échec de l'affirmation que deux chaînes sont égales. --- Attendu +++ Réel @@ @@ 'avery -veryl -ongwo -rd' + verylongword '
Pouvez-vous sentir le tandis que
boucle à venir? Eh bien, détrompez-vous. Est un tandis que
boucle le code le plus simple qui ferait passer le test?
Selon 'Transformation Priorities' (de Robert C. Martin), ce n'est pas le cas. La récursion est toujours plus simple qu'une boucle et il est beaucoup plus testable.
function wrap ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) renvoie substr ($ text, 0, $ lineLength). "\ n". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); return $ text;
Pouvez-vous même repérer le changement? C'était simple. Au lieu de concaténer avec le reste de la chaîne, nous avons concaténé avec la valeur de retour consistant à s’appeler avec le reste de la chaîne. Parfait!
Le prochain test le plus simple? Qu'en est-il deux mots peuvent envelopper, quand il y a un espace à la fin de la ligne.
function testItWrapsTwoWordsWhenSpaceAtTheEndOfLine () $ textToBeParsed = 'mot mot'; $ maxLineLength = 5; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Cela va bien. Cependant, la solution peut devenir un peu plus compliquée cette fois-ci.
Au début, vous pourriez vous référer à un str_replace ()
se débarrasser de l’espace et insérer une nouvelle ligne. Ne pas cette route mène à une impasse.
Le deuxième choix le plus évident serait un si
déclaration. Quelque chose comme ça:
function wrap ($ text, $ lineLength) if (strpos ($ text, ") == $ lineLength) renvoie substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength); if (strlen ($ text)> $ lineLength) renvoie substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); return $ text;
Cependant, cela entre dans une boucle sans fin, ce qui entraînera une erreur sur les tests.
Erreur irrécupérable PHP: la taille de mémoire autorisée de 134217728 octets est épuisée
Cette fois, il faut réfléchir! Le problème est que notre premier test a un texte avec une longueur de zéro. Également, strpos ()
renvoie false lorsqu'il ne peut pas trouver la chaîne. Comparer faux avec zéro… c'est? Il est vrai
. C'est mauvais pour nous car la boucle deviendra infinie. La solution? Changeons la première condition. Au lieu de rechercher un espace et de comparer sa position avec la longueur de la ligne, essayons plutôt de prendre directement le caractère à la position indiquée par la longueur de la ligne. Nous ferons un substr ()
un seul caractère, commençant au bon endroit dans le texte.
function wrap ($ text, $ lineLength) if (substr ($ text, $ lineLength - 1, 1) == ") renvoie substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength); if (strlen ($ text)> $ lineLength) renvoie substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); return $ text;
Mais que se passe-t-il si l'espace n'est pas juste en fin de ligne??
function testItWrapsTwoWordsWhenLineEndIsAfterFirstWord () $ textToBeParsed = 'mot mot'; $ maxLineLength = 7; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Hmm… nous devons revoir nos conditions. Je pense qu'après tout, nous aurons besoin de cette recherche pour la position du caractère d'espace.
function wrap ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) if (strpos (substr ($ text, 0, $ lineLength), ")! = 0) renvoie substr ($ text, 0 , strpos ($ text, ")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength); renvoyer substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap ($ text, $ lineLength), $ lineLength); return $ text;
Hou la la! Cela fonctionne réellement. Nous avons déplacé la première condition à l'intérieur de la seconde afin d'éviter la boucle sans fin et nous avons ajouté la recherche d'espace. Pourtant, il semble plutôt moche. Conditions imbriquées? Beurk. Il est temps de refactoriser.
fonction wrap ($ text, $ lineLength) if (strlen ($ text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strpos($text,")) . "\n" . $this->wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength); renvoie substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength);
C'est mieux mieux.
Rien de mal ne peut arriver à la suite de l'écriture d'un test.
Le test le plus simple suivant serait d'avoir trois mots qui se terminent sur trois lignes. Mais ce test réussit. Si vous écrivez un test quand vous savez que ça va passer? La plupart du temps, non. Mais si vous avez des doutes, ou si vous pouvez imaginer des modifications évidentes du code qui feraient échouer le nouveau test et laisser passer les autres, écrivez-le! Rien de mauvais ne peut arriver à la suite de l'écriture d'un test. En outre, considérez que vos tests sont votre documentation. Si votre test représente une partie essentielle de votre logique, écrivez-le!
De plus, le fait que les tests que nous avons obtenus ont réussi indique que nous nous approchons de la solution. De toute évidence, lorsque vous avez un algorithme de travail, tout test que nous écrivons passera.
Maintenant - trois mots sur deux lignes avec la ligne se terminant à l'intérieur du dernier mot; maintenant, ça échoue.
function testItWraps3WordsOn2Lines () $ textToBeParsed = 'mot mot mot'; $ maxLineLength = 12; $ this-> assertEquals ("mot word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Je m'attendais presque à ce que celui-ci fonctionne. Lorsque nous enquêtons sur l'erreur, nous obtenons:
Échec de l'affirmation que deux chaînes sont égales. --- Attendu +++ Réel @@ @@ -'word word -word '+' word + word word '
Oui. Nous devrions envelopper à l'extrême droite d'une ligne.
fonction wrap ($ text, $ lineLength) if (strlen ($ text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strrpos($text,")) . "\n" . $this->wrap (substr ($ text, strrpos ($ text, ") + 1), $ lineLength); renvoie substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength);
Il suffit de remplacer le strpos ()
avec strrpos ()
à l'intérieur de la seconde si
déclaration.
Les choses se compliquent. Il est assez difficile de trouver un test qui échoue… ou n'importe quel test, d'ailleurs, qui n'a pas encore été écrit.
C'est une indication que nous sommes assez proches d'une solution finale. Mais, hé, je viens de penser à un test qui échouera!
function testItWraps2WordsOn3Lines () $ textToBeParsed = 'mot mot'; $ maxLineLength = 3; $ this-> assertEquals ("wor \ nd \ nwor \ nd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Mais je me trompais. Ça passe. Hmm… Est-ce qu'on a fini? Attendre! Qu'en est-il de celui-ci?
function testItWraps2WordsAtBoundry () $ textToBeParsed = 'mot mot'; $ maxLineLength = 4; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Il échoue! Excellent. Lorsque la ligne a la même longueur que le mot, nous voulons que la deuxième ligne ne commence pas par un espace..
Échec de l'affirmation que deux chaînes sont égales. --- Attendu +++ Réel @@ @@ 'mot-mot' + wor + d '
Il y a plusieurs solutions. Nous pourrions introduire un autre si
déclaration pour vérifier l'espace de départ. Cela cadrerait avec le reste des conditions que nous avons créées. Mais n'y a-t-il pas une solution plus simple? Et si on venait réduire()
le texte?
function wrap ($ text, $ lineLength) $ text = trim ($ text); if (strlen ($ text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strrpos($text,")) . "\n" . $this->wrap (substr ($ text, strrpos ($ text, ") + 1), $ lineLength); renvoie substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength);
On y va.
À ce stade, je ne peux inventer aucun test qui échoue. Nous devons avoir fini! Nous utilisons maintenant TDD pour construire un algorithme simple, mais utile, de six lignes.
Quelques mots sur l'arrêt et "en cours". Si vous utilisez TDD, vous vous forcez à penser à toutes sortes de situations. Vous écrivez ensuite des tests pour ces situations et commencez à comprendre beaucoup mieux le problème. Habituellement, ce processus entraîne une connaissance intime de l'algorithme. Si vous ne pouvez pas penser à d’autres tests qui échouent, cela signifie-t-il que votre algorithme est parfait? Non nécessaire, sauf s'il existe un ensemble de règles prédéfini. TDD ne garantit pas un code sans bug; il vous aide simplement à écrire un meilleur code qui peut être mieux compris et modifié.
Mieux encore, si vous découvrez un bogue, il est d'autant plus facile d'écrire un test qui le reproduit. De cette façon, vous pouvez vous assurer que le bogue ne se reproduira plus jamais, car vous l'avez testé.!
Vous pouvez faire valoir que ce processus n'est pas techniquement "TDD". Et tu as raison! Cet exemple est plus proche du nombre de programmeurs quotidiens qui travaillent. Si vous voulez un véritable exemple du type "TDD", veuillez laisser un commentaire ci-dessous et je prévois d'en écrire un à l'avenir..
Merci d'avoir lu!