28 votes

Comment puis-je concevoir mes classes pour faciliter les tests unitaires?

Je vais vous avouer, je n'ai pas testé unitaire beaucoup... mais j'aimerais. Avec cela étant dit, j'ai une question très complexe processus d'inscription que j'aimerais optimiser pour faciliter les tests unitaires. Je suis à la recherche d'une structure de mes cours afin que je puisse les tester plus facilement dans l'avenir. Toute cette logique est contenue à l'intérieur d'un framework MVC, donc on peut penser que le contrôleur est la racine où tout est instancié à partir d'.

Pour simplifier, ce que je suis en demande, en substance, est de savoir comment l'installation d'un système où vous pouvez gérer un nombre quelconque de modules tiers avec CRUD mises à jour. Ces modules tiers sont toutes les API RESTful et piloté par les données de réponse est stockée dans des copies locales. Quelque chose comme la suppression d'un compte d'utilisateur aurait besoin de déclencher la suppression de tous les modules associés (j'entends en tant que fournisseurs). Ces fournisseurs peuvent avoir une dépendance à un autre prestataire, de sorte que l'ordre des suppressions/créations est important. Je suis intéressé dans la conception des modèles j'devraient être spécifiquement l'aide à l'appui de ma demande.

L'enregistrement s'étend sur plusieurs classes et stocke les données dans plusieurs tables db. Voici l'ordre des différents fournisseurs et les méthodes (ils ne sont pas statiques, écrit de cette façon pour des raisons de concision):

  1. Provider::create('external::create-user') lance l'enregistrement à une étape particulière d'un fournisseur particulier. Les deux points de syntaxe dans le premier paramètre indique la classe de déclencher la création sur providerClass::providerMethod. J'avais fait l'hypothèse générale que le Provider serait une interface avec les méthodes d' create(), update(), delete() que tous les autres fournisseurs de la mettre en œuvre. Comment cela est instanciée est probablement quelque chose que vous devez m'aider.
  2. $user = Provider_External::createUser() crée un utilisateur sur une API externe, renvoie succès, et l'utilisateur est stocké dans ma base de données.
  3. $customer = Provider_Gapps_Customer::create($user) crée un client sur un tiers de l'API, renvoie succès, et stocke localement.
  4. $subscription = Provider_Gapps_Subscription::create($customer) crée un abonnement associé pour le client sur la troisième partie de l'API, renvoie succès, et stocke localement.
  5. Provider_Gapps_Verification::get($customer, $subscription) récupère une ligne à partir d'une API externe. Cette information est stockée localement. Un autre appel est en fait ce qui je ne m'attarde pas à garder les choses concis.
  6. Provider_Gapps_Verification::verify($customer, $subscription) effectue une API externe du processus de vérification. Le résultat de ce qui est stocké localement.

C'est vraiment une bêtise de l'échantillon que le code s'appuie sur au moins 6 appels d'API externes et plus de 10 lignes de la base de données créée lors de l'inscription. Il n'est pas logique d'utiliser l'injection de dépendance au constructeur, parce que je pourrais avoir besoin d'instancier 6 classes dans le contrôleur, sans savoir si j'ai encore besoin d'eux tous. Ce que je cherche à accomplir serait quelque chose comme Provider::create('external') où j'ai tout simplement spécifier l'étape de départ pour lancer l'enregistrement.


Le Noeud du Problème

Donc, comme vous pouvez le voir, c'est juste un exemple d'un processus d'enregistrement. Je suis en train de construire un système où je pourrais avoir plusieurs centaines de fournisseurs de services (API externe modules) que j'ai besoin de vous inscrire pour, mettre à jour, supprimer, etc. Chacun de ces fournisseurs obtient liés à un compte d'utilisateur.

Je voudrais construire ce système d'une manière où je peux indiquer un ordre de priorité des opérations (mesures) lors du déclenchement de la création d'un nouveau fournisseur. Mettre une autre manière, permettez-moi de préciser quel fournisseur/méthode de combinaison est déclenchée suivant dans la chaîne des événements depuis la création peut s'étendre sur de nombreuses étapes. Actuellement, j'ai cette chaîne d'événements qui se déroulent par le sujet/pattern observer. Je suis à la recherche d'potentiellement déplacer ce code à une table de base de données, provider_steps, où j'ai la liste de chaque étape ainsi qu'il suit success_step et failure_step (pour les réductions et suppressions). Le tableau se présente comme suit:

  # the id of the parent provider row
  provider_id int(11) unsigned primary key,
  # the short, slug name of the step for using in codebase
  step_name varchar(60),
  # the name of the method correlating to the step
  method_name varchar(120),
  # the steps that get triggered on success of this step
  # can be comma delimited; multiple steps could be triggered in parallel
  triggers_success varchar(255),
  # the steps that get triggered on failure of this step
  # can be comma delimited; multiple steps could be triggered in parallel
  triggers_failure varchar(255),
  created_at datetime,
  updated_at datetime,
  index ('provider_id', 'step_name')

Il y a tellement de décisions à prendre ici... je sais que je devrais faveur de la composition au cours de l'héritage et de créer des interfaces. Je sais aussi que je suis susceptible allez avoir besoin des usines. Enfin, j'ai beaucoup de modèle de domaine de la merde qui se passe ici... alors j'ai probablement besoin d'une entreprise de classes du domaine. Je ne suis pas sûr de savoir comment maille sans la création d'un véritable désordre dans ma quête du saint graal.

Aussi, où serait le meilleur endroit pour les requêtes db à prendre place?

J'ai un modèle pour chaque table de base de données déjà, mais je suis intéressé de savoir où et comment instancier le modèle particulier de méthodes.

Les choses que j'ai lu...

6voto

John Deters Points 2795

Vous travaillez déjà avec la pub/sub modèle, qui semble le plus approprié. Compte tenu de rien, mais votre commentaires ci-dessus, je serais l'examen d'une liste ordonnée comme une priorité mécanisme.

Mais il n'est toujours pas en odeur de droit que chaque abonné est concerné par l'ordre des opérations de ses personnes à charge pour le déclenchement de succès/échec. Dépendances généralement semblent comme ils appartiennent à un arbre, pas une liste. Si vous avez stocké dans un arbre (à l'aide du diagramme composite), puis intégré dans la récursivité serait capable de nettoyer chaque dépendance en nettoyant ses personnes à charge en premier. De cette façon, vous n'êtes plus inquiet au sujet de la hiérarchisation de l'ordre dans lequel le nettoyage se fait - l'arbre des poignées automatiquement.

Et vous pouvez utiliser un arbre pour le stockage de pub/sub abonnés presque aussi facilement que vous pouvez utiliser une liste.

À l'aide d'un test-driven development approche pourrait obtenir ce dont vous avez besoin, et de garantir l'ensemble de votre application n'est pas seulement entièrement vérifiable, mais complètement couverts par des tests qui prouvent qu'il fait ce que vous voulez. J'aimerais commencer par vous décrire exactement ce que vous devez faire pour répondre à une seule condition.

Une chose que vous savez que vous voulez faire est d'ajouter un fournisseur, un TestAddProvider() test semble le plus approprié. Notez qu'il devrait être assez simple à ce point, et n'ont rien à voir avec un composite modèle. Une fois que cela fonctionne, vous savez qu'un fournisseur a une personne à charge. Créer un TestAddProviderWithDependent() de test, et voir comment ça se passe. Encore une fois, il ne devrait pas être complexe. Ensuite, vous auriez probablement envie de TestAddProviderWithTwoDependents(), et c'est là que la liste devait être mis en œuvre. Une fois que le travail, vous savez que vous voulez le Fournisseur pour être également une personne à charge, de sorte qu'un nouveau test de prouver le modèle d'héritage travaillé. À partir de là, vous ajoutez suffisamment de tests pour vous convaincre que diverses combinaisons de l'ajout de fournisseurs et de personnes à charge travaillé, et des tests pour des conditions d'exception, etc. Juste des essais et exigences, vous seriez rapidement arriver à une composite modèle qui répond à vos besoins. À ce point, je l'avais fait craquer ma copie de GoF pour m'assurer de comprendre les conséquences du choix du composite modèle, et pour m'assurer que je n'ai pas ajouter inappropriées de la verrue.

Une autre exigence est de supprimer les fournisseurs, afin de créer un TestDeleteProvider() de tester et de mettre en œuvre les DeleteProvider() la méthode. Vous ne serez pas loin d'avoir le fournisseur de supprimer ses personnes à charge, trop, de sorte que la prochaine étape pourrait être la création d'un TestDeleteProviderWithADependent() de test. La récursivité du composite modèle devrait être évident, à ce point, et vous devriez seulement besoin d'un peu plus de tests pour les convaincre vous-même que profondément imbriqués les fournisseurs, vide leafs, à l'échelle de nœuds, etc., tout va bien nettoyer eux-mêmes.

Je suppose qu'il ya une exigence pour vos fournisseurs à fait fournir leurs services. Le temps de tester en appelant les fournisseurs (à l'aide de se moquer de fournisseurs pour les tests), et l'ajout de tests que de s'assurer qu'ils peuvent trouver leurs dépendances. Encore une fois, la récursivité du composite modèle devrait aider à construire la liste des dépendances ou tout ce que vous avez besoin d'appeler le bon fournisseurs correctement.

Vous trouverez peut-être que les fournisseurs doivent être appelés dans un ordre spécifique. À ce stade, vous pourriez avoir besoin d'ajouter de priorisation pour les listes à chaque nœud dans l'arbre composite. Ou peut-être que vous avez à construire une tout autre structure (comme une liste chaînée) à les appeler dans le bon ordre. Utiliser les tests et approche lentement. Vous pourriez encore avoir des personnes concernées que vous supprimez des personnes à charge en particulier de l'extérieur ordre prescrit. À ce stade, vous pouvez utiliser vos tests pour prouver aux sceptiques que vous aurez toujours les supprimer en toute sécurité, même si ce n'est dans l'ordre de la pensée.

Si vous l'avez fait à droite, tous les essais doivent continuer à passer.

Puis viennent les questions pièges. Que faire si vous avez deux fournisseurs qui partagent une dépendance? Si vous supprimez un fournisseur, faut-il supprimer l'ensemble de ses dépendances, même si un autre fournisseur besoins de l'un d'eux? Ajouter un test, et de mettre en œuvre votre règle. Je me dis que je serais le manipuler via le comptage de référence, mais peut-être que vous voulez une copie du prestataire pour la seconde instance, de sorte que vous n'aurez jamais à vous soucier de partage des enfants, et vous de garder les choses le plus simple de cette façon. Ou peut-être il n'est jamais un problème dans votre domaine. Une autre question délicate est de savoir si vos fournisseurs peuvent avoir des dépendances circulaires. Comment vous assurez-vous que vous ne finissent pas dans une auto-référentielle de la boucle? Écrire des tests et de le comprendre.

Après vous avez cette structure entière compris, c'est alors seulement que vous commencez à penser au sujet des données que vous utiliseriez pour décrire cette hiérarchie.

C'est l'approche que je voudrais examiner. Il peut ne pas être bon pour vous, mais c'est à vous de décider.

4voto

Bubba Points 2826

Les Tests Unitaires Avec les tests unitaires, nous ne voulons tester le code qui fait l'unité de code source, généralement une méthode d'une classe ou d'une fonction en PHP (Tests Unitaires vue d'ensemble). Ce qui indique que nous ne voulons pas fait de test de l'API externe dans les Tests Unitaires, nous ne voulons tester le code que nous écrivons localement. Si vous ne voulez tester ensemble du flux de travail, vous êtes probablement désireux d'effectuer des tests d'intégration (Tests d'Intégration Aperçu), qui est une bête différente.

Comme vous l'avez demandé spécifiquement sur la conception pour les Tests Unitaires, supposons que vous vraiment dire les Tests Unitaires, par opposition à des Tests d'Intégration et de soumettre des qu'il y a deux façons raisonnables pour aller sur la conception de votre Fournisseur de classes.

Talon La pratique de remplacer un objet par un test du double (en option) renvoie configuré valeurs de retour est pris comme cogner. Vous pouvez utiliser un tampon pour "remplacer un composant réel sur lequel le SUT dépend donc que le test a un point de contrôle pour les apports indirects de la CUS. Cela permet le test de la force de la CUS bas des chemins qu'il pourrait autrement ne pas exécuter". De Référence Et Exemples

Les Objets Fantaisie La pratique de remplacer un objet par un test double qui vérifie les attentes, par exemple en affirmant qu'une méthode a été appelé, est désigné sous les moqueries.

Vous pouvez utiliser un objet fantaisie "comme un point d'observation qui est utilisé pour vérifier l'indirects sorties de la SUT qu'il est exercé. Généralement, le simulacre de l'objet comprend également la fonctionnalité d'un test de stub qu'il doit revenir aux valeurs de la SUT si elle n'a pas déjà échoué les tests, mais l'accent est mis sur la vérification de l'indirect sorties. Par conséquent, un objet fantaisie est beaucoup plus que juste un test de stub plus assertions; il est utilisé d'une façon fondamentalement différente". De Référence Et Exemples

Nos Conseils La conception de votre classe à la fois à tous les deux Stubbing et les Moqueries. Le PHP Manuel de l'Unité est un excellent exemple de Cogner et se moquant de Service Web. Alors que ce n'est pas vous aider à sortir de la boîte, il montre comment vous allez sur la mise en œuvre de même pour l'API Restful que vous consommez.

Où est le meilleur endroit pour les requêtes db à prendre place? Nous vous suggérons d'utiliser un ORM et de ne pas résoudre vous-même. Vous pouvez facilement Google PHP ORM et de prendre votre décision basée sur vos propres besoins; notre conseil est d'utiliser la Doctrine , parce que nous utilisons la Doctrine et elle convient bien à nos besoins, et au cours des quelques dernières années, nous avons appris à apprécier comment la Doctrine développeurs savent le domaine, tout simplement, ils le font mieux que nous pourrions le faire nous-mêmes nous sommes donc heureux de le faire pour nous.

Si vous n'avez pas vraiment à comprendre pourquoi vous devez utiliser un ORM, voir Pourquoi devriez-vous utiliser un ORM? et puis Google la même question. Si vous vous sentez toujours comme vous pouvez rouler vos propres ORM ou gérer l'Accès de Base de données vous-même mieux que le gars qui lui sont consacrées, nous nous attendrions à ce que vous savez déjà la réponse à la question. Si vous sentez que vous avez un besoin pressant de le gérer vous-même, nous vous suggérons de regarder le code source pour un certain nombre de de l'ORM (Doctrine sur Github) et de trouver la solution qui correspond le mieux à votre scénario.

Merci de demander un plaisir question, je l'apprécie.

2voto

bolbol Points 170

Chaque relation de dépendance unique au sein de votre hiérarchie de classes doit être accessible depuis le monde extérieur (ne doit pas être fortement couplée). Par exemple, si vous instanciez la classe A dans la classe B, la classe B doit avoir des méthodes setter / getter implémentées pour le détenteur d'instance de classe A dans la classe B.

http://en.wikipedia.org/wiki/Dependency_injection

2voto

hakre Points 102271

Le plus éloigné problème que je peux voir avec ton code et cela vous empêche de le tester, en fait - est prise de l'utilisation de la méthode de classe statique appels:

  • Provider::create('external::create-user')
  • $user = Provider_External::createUser()
  • $customer = Provider_Gapps_Customer::create($user)
  • $subscription = Provider_Gapps_Subscription::create($customer)
  • ...

C'est une épidémie dans votre code, même si vous "seulement" décrites comme statique pour la "brièveté". Ces attitiude n'est pas souci de concision, il est contre-productif pour de code de tests. Éviter à tout prix incl. lorsque vous posez une question à propos de Tests Unitaires, c'est une mauvaise pratique et il est connu que ce type de code est difficile à tester.

Après avoir converti tous les appels statiques en objet invocations de méthode et utilisé l'Injection de Dépendance au lieu de statique de l'état global de passer les objets le long, vous pouvez tout simplement faire des tests unitaires avec PHPUnit incl. faisant usage de talon et les objets fantaisie de collaboration dans votre (simple) des tests.

Voici donc une TODO:

  1. Refactoriser statique les appels de méthode dans l'objet des appels de méthode.
  2. Utiliser l'Injection de Dépendance pour passer des objets le long.

Et vous avez très bien l'amélioration de votre code. Si vous faites valoir que vous ne pouvez pas le faire, ne perdez pas votre temps avec des tests unitaires, des déchets avec le maintien de votre application, navire rapide, le laisser faire de l'argent, et de le graver si c'est pas rentable du tout plus. Mais ne perdez pas votre la programmation la vie de l'unité-essai statique de l'état global - c'est juste stupide à faire.

1voto

Nitin Tripathi Points 361

Pensez à la superposition de votre application avec des rôles et responsabilités définis pour chaque couche. Vous pouvez, comme pour prendre de l'inspiration à partir de Apache Axis flux de messages du sous-système. L'idée de base est de créer une chaîne de gestionnaires par le biais de laquelle la demande de flux jusqu'à ce qu'elle soit traitée. Une telle conception facilite plugable composants qui peuvent être réunis pour créer des fonctions d'ordre supérieur.

En outre, vous pouvez avoir envie de lire à propos de Foncteurs/la Fonction des Objets, en particulier de la Fermeture, de Prédicat, le Transformateur et le Fournisseur pour créer votre les composants. Espérons que cela aide.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X