Supposons que j'ai une classe (pardonnez l'exemple artificiel et sa mauvaise conception):
class MyProfit
{
public decimal GetNewYorkRevenue();
public decimal GetNewYorkExpenses();
public decimal GetNewYorkProfit();
public decimal GetMiamiRevenue();
public decimal GetMiamiExpenses();
public decimal GetMiamiProfit();
public bool BothCitiesProfitable();
}
(Notez que les méthodes GetxxxRevenue () et GetxxxExpenses () ont des dépendances qui sont tronquées)
Je teste maintenant à la fois BothCitiesProfitable () qui dépend de GetNewYorkProfit () et GetMiamiProfit (). Est-il correct de bloquer GetNewYorkProfit () et GetMiamiProfit ()?
Il semble que si je ne le fais pas, je teste simultanément GetNewYorkProfit () et GetMiamiProfit () avec BothCitiesProfitable (). Je dois m'assurer de configurer le stubbing pour GetxxxRevenue () et GetxxxExpenses () afin que les méthodes GetxxxProfit () renvoient les valeurs correctes.
Jusqu'à présent, je n'ai vu que des exemples de dépendances de stubbing sur des classes externes et non sur des méthodes internes.
Et si ça va, y a-t-il un modèle particulier que je devrais utiliser pour ce faire?
MISE À JOUR
Je crains que nous ne manquions pas le problème principal et c'est probablement la faute de mon pauvre exemple. La question fondamentale est la suivante: si une méthode d'une classe dépend d'une autre méthode exposée publiquement dans cette même classe, est-il acceptable (ou même recommandé) de bloquer cette autre méthode?
Peut-être que je manque quelque chose, mais je ne suis pas sûr que la séparation des cours ait toujours un sens. Peut-être un autre exemple minimalement meilleur serait:
class Person
{
public string FirstName()
public string LastName()
public string FullName()
}
où le nom complet est défini comme:
public string FullName()
{
return FirstName() + " " + LastName();
}
Est-il correct de bloquer FirstName () et LastName () lors du test de FullName ()?
la source
Réponses:
Vous devez séparer la classe en question.
Chaque classe doit effectuer une tâche simple. Si votre tâche est trop compliquée à tester, alors la tâche de la classe est trop grande.
Ignorant la maladresse de cette conception:
MISE À JOUR
Le problème avec les méthodes de stubbing dans une classe est que vous violez l'encapsulation. Votre test doit vérifier si le comportement externe de l'objet correspond ou non aux spécifications. Tout ce qui se passe à l'intérieur de l'objet ne le regarde pas.
Le fait que FullName utilise FirstName et LastName est un détail d'implémentation. Rien en dehors de la classe ne devrait s'en soucier. En se moquant des méthodes publiques afin de tester l'objet, vous faites une hypothèse sur cet objet est implémenté.
À un certain moment dans l'avenir, cette hypothèse pourrait cesser d'être correcte. Peut-être que toute la logique du nom sera déplacée vers un objet Name que Person appelle simplement. Peut-être que FullName accédera directement aux variables membres first_name et last_name au lieu d'appeler FirstName et LastName.
La deuxième question est de savoir pourquoi vous en ressentez le besoin. Après tout, votre classe individuelle pourrait être testée quelque chose comme:
Vous ne devriez pas ressentir le besoin de bloquer quoi que ce soit pour cet exemple. Si vous le faites, vous êtes heureux et bien ... arrêtez! Il n'y a aucun avantage à se moquer des méthodes car vous avez de toute façon le contrôle de ce qui s'y trouve.
Le seul cas où il semblerait logique que les méthodes utilisées par FullName soient moquées est si en quelque sorte FirstName () et LastName () étaient des opérations non triviales. Peut-être que vous écrivez l'un de ces générateurs de noms aléatoires, ou que FirstName et LastName interrogent la base de données pour une réponse, ou quelque chose. Mais si c'est ce qui se passe, cela suggère que l'objet fait quelque chose qui n'appartient pas à la classe Person.
Autrement dit, se moquer des méthodes, c'est prendre l'objet et le diviser en deux. Une pièce est moquée tandis que l'autre pièce est testée. Ce que vous faites est essentiellement une décomposition ad hoc de l'objet. Si c'est le cas, il suffit de casser déjà l'objet.
Si votre classe est simple, vous ne devriez pas ressentir le besoin d'en moquer des morceaux pendant un test. Si votre classe est suffisamment complexe pour que vous ressentiez le besoin de vous moquer, alors vous devez diviser la classe en morceaux plus simples.
MISE À JOUR ENCORE
D'après moi, un objet a un comportement externe et interne. Le comportement externe inclut les appels de valeurs de retour dans d'autres objets, etc. Évidemment, tout élément de cette catégorie doit être testé. (sinon que testeriez-vous?) Mais le comportement interne ne devrait pas vraiment être testé.
Maintenant, le comportement interne est testé, car c'est ce qui entraîne le comportement externe. Mais je n'écris pas de tests directement sur le comportement interne, mais indirectement via le comportement externe.
Si je veux tester quelque chose, je pense qu'il doit être déplacé pour qu'il devienne un comportement externe. C'est pourquoi je pense que si vous voulez vous moquer de quelque chose, vous devez diviser l'objet afin que la chose que vous voulez moquer se trouve maintenant dans le comportement externe des objets en question.
Mais quelle différence cela fait-il? Si FirstName () et LastName () sont membres d'un autre objet, cela change-t-il vraiment le problème de FullName ()? Si nous décidons qu'il est nécessaire de se moquer de FirstName et LastName, est-ce réellement utile pour eux d'être sur un autre objet?
Je pense que si vous utilisez votre approche moqueuse, vous créez une couture dans l'objet. Vous disposez de fonctions telles que FirstName () et LastName () qui communiquent directement avec une source de données externe. Vous avez également FullName () qui n'en a pas. Mais comme ils sont tous dans la même classe, cela n'apparaît pas. Certaines pièces ne sont pas censées accéder directement à la source de données et d'autres le sont. Votre code sera plus clair si vous divisez ces deux groupes.
MODIFIER
Prenons un peu de recul et demandons: pourquoi nous moquons-nous des objets lorsque nous testons?
Maintenant, je pense que les raisons 1 à 4 ne s'appliquent pas à ce scénario. Se moquer de la source externe lors du test de fullname prend en charge toutes ces raisons de se moquer. La seule pièce non manipulée qui est la simplicité, mais il semble que l'objet soit assez simple ce n'est pas un souci.
Je pense que votre préoccupation est la raison numéro 5. La préoccupation est qu'à un moment donné, changer la mise en œuvre de FirstName et LastName rompra le test. À l'avenir, FirstName et LastName peuvent obtenir les noms à partir d'un emplacement ou d'une source différente. Mais FullName le sera probablement toujours
FirstName() + " " + LastName()
. C'est pourquoi vous voulez tester FullName en se moquant de FirstName et LastName.Ce que vous avez alors, c'est un sous-ensemble de l'objet personne qui est plus susceptible de changer que les autres. Le reste de l'objet utilise ce sous-ensemble. Ce sous-ensemble récupère actuellement ses données à l'aide d'une seule source, mais peut récupérer ces données d'une manière complètement différente à une date ultérieure. Mais pour moi, il semble que ce sous-ensemble soit un objet distinct essayant de sortir.
Il me semble que si vous vous moquez de la méthode de l'objet, vous divisez l'objet. Mais vous le faites de manière ponctuelle. Votre code ne précise pas qu'il existe deux éléments distincts à l'intérieur de votre objet Personne. Il suffit donc de diviser cet objet dans votre code réel, afin qu'il soit clair à la lecture de votre code ce qui se passe. Choisissez la division réelle de l'objet qui a du sens et n'essayez pas de diviser l'objet différemment pour chaque test.
Je soupçonne que vous pourriez vous opposer à diviser votre objet, mais pourquoi?
MODIFIER
J'avais tort.
Vous devez fractionner les objets plutôt que d'introduire des séparations ad hoc en se moquant des méthodes individuelles. Cependant, j'étais trop concentré sur la seule méthode de division des objets. Cependant, OO propose plusieurs méthodes de fractionnement d'un objet.
Ce que je proposerais:
C'est peut-être ce que vous faisiez depuis le début. Mais je ne pense pas que cette méthode aura les problèmes que j'ai vus avec les méthodes de simulation parce que nous avons clairement défini de quel côté chaque méthode est. Et en utilisant l'héritage, nous évitons la gêne qui pourrait survenir si nous utilisions un objet wrapper supplémentaire.
Cela introduit une certaine complexité, et pour seulement quelques fonctions utilitaires, je les testerais probablement en se moquant de la source tierce sous-jacente. Bien sûr, ils courent un risque accru de rupture, mais cela ne vaut pas la peine d'être réorganisé. Si vous avez un objet assez complexe dont vous avez besoin pour le diviser, alors je pense que quelque chose comme ça est une bonne idée.
la source
Bien que je sois d'accord avec la réponse de Winston Ewert, il n'est parfois tout simplement pas possible de diviser une classe, que ce soit en raison de contraintes de temps, de l'API déjà utilisée ailleurs ou de ce que vous avez.
Dans un cas comme celui-ci, au lieu de méthodes moqueuses, j'écrirais mes tests pour couvrir la classe de manière incrémentielle. Testez les méthodes
getExpenses()
etgetRevenue()
, puis testez lesgetProfit()
méthodes, puis testez les méthodes partagées. Il est vrai que vous aurez plus d'un test couvrant une méthode particulière, mais puisque vous avez écrit des tests réussis pour couvrir les méthodes individuellement, vous pouvez être sûr que vos sorties sont fiables compte tenu d'une entrée testée.la source
Pour simplifier davantage vos exemples, disons que vous testez
C()
, ce qui dépend deA()
etB()
, chacun ayant ses propres dépendances. OMI, cela revient à ce que votre test tente de réaliser.Si vous testez le comportement de comportements
C()
connus donnés deA()
etB()
alors il est probablement plus facile et meilleur de se détacherA()
etB()
. Ce serait probablement appelé un test unitaire par les puristes.Si vous testez le comportement de l'ensemble du système (du
C()
point de vue de), je quitteraisA()
etB()
tel qu'implémenté et soit bloquer leurs dépendances (si cela permet de tester) ou configurer un environnement sandbox, tel qu'un test base de données. J'appellerais cela un test d'intégration.Les deux approches peuvent être valides, donc si elles sont conçues pour que vous puissiez les tester dans un sens ou dans l'autre, ce sera probablement mieux à long terme.
la source