Les cadres fournissent un outil pour le développement rapide d'applications, mais génèrent souvent une dette technique aussi rapidement qu'ils vous permettent de créer des fonctionnalités. La dette technique est créée lorsque la maintenabilité n’est pas une priorité du développeur. Les modifications et le débogage futurs deviennent coûteux, en raison du manque de tests unitaires et de structure..
Voici comment commencer à structurer votre code pour atteindre la testabilité et la maintenabilité - et vous faire gagner du temps..
Commençons par un code artificiel, mais typique. Cela pourrait être une classe modèle dans n'importe quel framework.
class User fonction publique getCurrentUser () $ user_id = $ _SESSION ['user_id']; $ user = App :: db-> select ('id, nom d'utilisateur') -> où ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false;
Ce code fonctionnera, mais doit être amélioré:
$ _SESSION
variable globale. Les frameworks de tests unitaires, tels que PHPUnit, reposent sur la ligne de commande, où $ _SESSION
et beaucoup d'autres variables globales ne sont pas disponibles.App :: db
utilisé dans notre application. Aussi, qu'en est-il des cas où nous ne voulons pas que les informations de l'utilisateur actuel?Voici une tentative pour créer un test unitaire pour la fonctionnalité ci-dessus.
La classe UserModelTest étend PHPUnit_Framework_TestCase fonction publique testGetUser () $ user = new User (); $ currentUser = $ user-> getCurrentUser (); $ this-> assertEquals (1, $ currentUser-> id);
Examinons cela. Tout d'abord, le test échouera. le $ _SESSION
variable utilisée dans le Utilisateur
L'objet n'existe pas dans un test unitaire, car il exécute PHP en ligne de commande..
Deuxièmement, il n'y a pas de configuration de connexion de base de données. Cela signifie que, pour que cela fonctionne, nous devrons initialiser notre application afin d’obtenir la App
objet et son db
objet. Nous aurons également besoin d’une connexion à la base de données pour pouvoir tester.
Pour que ce test unitaire fonctionne, il faudrait:
Alors, voyons comment nous pouvons améliorer cela.
La fonction de récupération de l'utilisateur actuel est inutile dans ce contexte simple. Ceci est un exemple artificiel, mais dans l’esprit des principes de DRY, la première optimisation que j’ai choisie est de généraliser cette méthode..
class User fonction publique getUser ($ user_id) $ user = App :: db-> select ('utilisateur') -> où ('id', $ utilisateur_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false;
Ceci fournit une méthode que nous pouvons utiliser dans l’ensemble de notre application. Nous pouvons transmettre l'utilisateur actuel au moment de l'appel, plutôt que de transmettre cette fonctionnalité au modèle. Le code est plus modulaire et maintenable lorsqu'il ne s'appuie pas sur d'autres fonctionnalités (telles que la variable globale de session).
Cependant, cela n’est toujours pas testable et maintenable comme cela pourrait être. Nous comptons toujours sur la connexion à la base de données.
Aidons à améliorer la situation en ajoutant des injections de dépendance. Voici à quoi notre modèle pourrait ressembler, lorsque nous passons la connexion de base de données à la classe.
utilisateur de classe protected $ _db; fonction publique __construct ($ db_connection) $ this -> _ db = $ db_connection; public function getUser ($ user_id) $ user = $ this -> _ db-> select ('user') -> où ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false;
Maintenant, les dépendances de nos Utilisateur
modèle sont fournis pour. Notre classe n'assume plus une certaine connexion à la base de données, ni ne repose sur des objets globaux.
À ce stade, notre classe est fondamentalement testable. Nous pouvons passer une source de données de notre choix (la plupart du temps) et un identifiant d'utilisateur, et tester les résultats de cet appel. Nous pouvons également déconnecter des connexions de base de données distinctes (en supposant que les deux méthodes implémentent les mêmes méthodes de récupération des données). Cool.
Regardons à quoi pourrait ressembler un test unitaire pour cela.
_mockDb (); $ utilisateur = nouvel utilisateur ($ db_connection); $ resultat = $ user-> getUser (1); $ attendus = new StdClass (); $ attendu-> id = 1; $ attendue-> nom d'utilisateur = 'fideloper'; $ this-> assertEquals ($ result-> id, $ attendu-> id, 'ID utilisateur correctement défini'); $ this-> assertEquals ($ result-> nom d'utilisateur, $ attendu-> nom d'utilisateur, 'Nom d'utilisateur correctement défini'); protected function _mockDb () // "Mock" (stub) objet de résultat de ligne de base de données $ returnResult = new StdClass (); $ returnResult-> id = 1; $ returnResult-> username = 'fideloper'; // Objet de résultat de base de données simulé $ result = m :: mock ('DbResult'); $ result-> shouldReceive ('num_results') -> once () -> andReturn (1); $ result-> shouldReceive ('row') -> once () -> andReturn ($ returnResult); // Objet de connexion à la base de données simulée $ db = m :: mock ('DbConnection'); $ db-> shouldReceive ('select') -> once () -> andReturn ($ db); $ db-> shouldReceive ('Where') -> once () -> andReturn ($ db); $ db-> shouldReceive ('limit') -> once () -> andReturn ($ db); $ db-> shouldReceive ('get') -> once () -> andReturn ($ result); return $ db;
J'ai ajouté quelque chose de nouveau à ce test unitaire: Mockery. Mockery vous permet de "simuler" (de faux) objets PHP. Dans ce cas, nous nous moquons de la connexion à la base de données. Avec notre maquette, nous pouvons ignorer le test d'une connexion à une base de données et simplement tester notre modèle.
Envie d'en savoir plus sur Mockery?
Dans ce cas, nous nous moquons d'une connexion SQL. Nous disons à l'objet fictif de s'attendre à avoir le sélectionner
, où
, limite
et obtenir
méthodes appelées dessus. Je retourne le Mock, lui-même, pour refléter le retour de l'objet connexion SQL ($ this
), rendant ainsi sa méthode appelée "chainable". Notez que pour le obtenir
méthode, je retourne le résultat de l'appel de base de données - un stdClass
objet avec les données d'utilisateur remplies.
Cela résout quelques problèmes:
Nous pouvons encore faire beaucoup mieux. Voici où ça devient intéressant.
Pour améliorer cela, nous pourrions définir et implémenter une interface. Considérons le code suivant.
interface UserRepositoryInterface fonction publique getUser ($ id_utilisateur); La classe MysqlUserRepository implémente UserRepositoryInterface protected $ _db; fonction publique __construct ($ db_conn) $ this -> _ db = $ db_conn; public function getUser ($ user_id) $ user = $ this -> _ db-> select ('user') -> où ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false; utilisateur de classe protected $ userStore; fonction publique __construct (UserRepositoryInterface $ user) $ this-> userStore = $ user; fonction publique getUser ($ user_id) return $ this-> userStore-> getUser ($ user_id);
Il y a quelques choses qui se passent ici.
addUser ()
méthode.Interface utilisateur
dans notre Utilisateur
modèle. Cela garantit que la source de données aura toujours une getUser ()
méthode disponible, quelle que soit la source de données utilisée pour la mise en œuvre Interface utilisateur
.Notez que notre
Utilisateur
indications de type d'objetInterface utilisateur
dans son constructeur. Cela signifie qu'une classe implémentantInterface utilisateur
DOIT être passé dans leUtilisateur
objet. C’est une garantie sur laquelle nous comptons - nous avons besoin de lagetUser
méthode pour toujours être disponible.
Quel est le résultat de cette?
Utilisateur
classe, nous pouvons facilement nous moquer de la source de données. (Tester les implémentations de la source de données serait le travail d'un test unitaire séparé).Utilisateur
objet si nous avons besoin de. Si vous décidez d’abandonner SQL, vous pouvez simplement créer une autre implémentation (par exemple, MongoDbUser
) et passez cela dans votre Utilisateur
modèle.Nous avons également simplifié notre test unitaire!
_mockUserRepo (); $ user = nouvel utilisateur ($ userRepo); $ resultat = $ user-> getUser (1); $ attendus = new StdClass (); $ attendu-> id = 1; $ attendue-> nom d'utilisateur = 'fideloper'; $ this-> assertEquals ($ result-> id, $ attendu-> id, 'ID utilisateur correctement défini'); $ this-> assertEquals ($ result-> nom d'utilisateur, $ attendu-> nom d'utilisateur, 'Nom d'utilisateur correctement défini'); fonction protégée _mockUserRepo () // résultat fictif attendu $ result = new StdClass (); $ résultat-> id = 1; $ result-> username = 'fideloper'; // Mock n'importe quel référentiel d'utilisateurs $ userRepo = m :: mock ('Fideloper \ Third \ Repository \ UserRepositoryInterface'); $ userRepo-> shouldReceive ('getUser') -> once () -> andReturn ($ result); return $ userRepo;
Nous avons complètement mis en place une simulation de connexion à une base de données. Au lieu de cela, nous nous moquons simplement de la source de données et lui disons quoi faire quand getUser
est appelé.
Mais on peut encore faire mieux!
Considérez l'utilisation de notre code actuel:
// Dans certains contrôleurs $ user = new User (new MysqlUser (App: db-> getConnection ("mysql"))); $ user-> id = App :: session ("utilisateur-> id"); $ currentUser = $ user-> getUser ($ user_id);
Notre dernière étape sera de présenter des conteneurs. Dans le code ci-dessus, nous devons créer et utiliser un ensemble d'objets uniquement pour obtenir notre utilisateur actuel. Ce code peut être encombré dans votre application. Si vous devez passer de MySQL à MongoDB, vous aurez encore besoin de modifier chaque endroit où le code ci-dessus apparaît. C'est à peine sec. Les conteneurs peuvent résoudre ce problème.
Un conteneur "contient" simplement un objet ou une fonctionnalité. Cela ressemble à un registre dans votre application. Nous pouvons utiliser un conteneur pour instancier automatiquement un nouveau Utilisateur
objet avec toutes les dépendances nécessaires. Ci-dessous, j'utilise Pimple, une classe de conteneur populaire.
// Quelque part dans un fichier de configuration $ container = new Pimple (); $ conteneur ["utilisateur"] = fonction () retourne nouvel utilisateur (nouveau MysqlUser (App: db-> getConnection ('mysql'))); // Maintenant, dans tous nos contrôleurs, nous pouvons simplement écrire: $ currentUser = $ container ['user'] -> getUser (App :: session ('user_id'));
J'ai déménagé la création du Utilisateur
modéliser en un seul emplacement dans la configuration de l'application. Par conséquent:
Utilisateur
objet et le magasin de données de choix est défini dans un emplacement dans notre application.Utilisateur
modèle d’utilisation de MySQL vers n’importe quelle autre source de données UN emplacement. C'est beaucoup plus maintenable.Au cours de ce tutoriel, nous avons réalisé les tâches suivantes:
Je suis sûr que vous avez remarqué que nous avons ajouté beaucoup plus de code sous le nom de maintenabilité et de testabilité. Un argument fort peut être avancé contre cette implémentation: nous augmentons la complexité. En effet, cela nécessite une connaissance approfondie du code, à la fois pour l'auteur principal et pour les collaborateurs d'un projet..
Cependant, le coût de l'explication et de la compréhension est largement dépassé par le coût global supplémentaire diminution en dette technique.
Vous pouvez inclure Moquerie et PHPUnit dans votre application facilement en utilisant Composer. Ajoutez-les à votre section "require-dev" dans votre composer.json
fichier:
"require-dev": "moquerie / moquerie": "0.8. *", "phpunit / phpunit": "3.7. *"
Vous pouvez ensuite installer vos dépendances Composer avec les exigences "dev":
$ php composer.phar installer --dev
En savoir plus sur Mockery, Composer et PHPUnit ici sur Nettuts+.
Pour PHP, pensez à utiliser Laravel 4, car il utilise exceptionnellement les conteneurs et autres concepts décrits ici..
Merci d'avoir lu!