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:
class NewYork
{
decimal GetRevenue();
decimal GetExpenses();
decimal GetProfit();
}
class Miami
{
decimal GetRevenue();
decimal GetExpenses();
decimal GetProfit();
}
class MyProfit
{
MyProfit(NewYork new_york, Miami miami);
boolean bothProfitable();
}
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:
Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");
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?
- Faites en sorte que les tests s'exécutent de manière cohérente (évitez d'accéder à des éléments qui changent d'une exécution à l'autre)
- Évitez d'accéder à des ressources coûteuses (ne touchez pas à des services tiers, etc.)
- Simplifiez le système testé
- Facilitez le test de tous les scénarios possibles (c'est-à-dire des choses comme la simulation de l'échec, etc.)
- Évitez de dépendre des détails d'autres morceaux de code afin que les changements dans ces autres morceaux de code ne cassent pas ce test.
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:
class PersonBase
{
abstract sring FirstName();
abstract string LastName();
string FullName()
{
return FirstName() + " " + LastName();
}
}
class Person extends PersonBase
{
string FirstName();
string LastName();
}
class FakePerson extends PersonBase
{
void setFirstName(string);
void setLastName(string);
string getFirstName();
string getLastName();
}
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.