J'ai commencé à écrire des tests unitaires pour mon projet actuel. Je n'ai pas vraiment d'expérience avec cela cependant. Je veux d’abord complètement "comprendre", donc je n’utilise actuellement ni mon framework IoC ni une bibliothèque moqueuse.
Je me demandais s'il y avait un problème avec la fourniture d'arguments nuls aux constructeurs des objets dans les tests unitaires. Laissez-moi vous donner un exemple de code:
public class CarRadio
{...}
public class Motor
{
public void SetSpeed(float speed){...}
}
public class Car
{
public Car(CarRadio carRadio, Motor motor){...}
}
public class SpeedLimit
{
public bool IsViolatedBy(Car car){...}
}
Encore un autre exemple de code de voiture (MC), réduit aux seules parties importantes de la question. J'ai maintenant écrit un test qui ressemble à ceci:
public class SpeedLimitTest
{
public void TestSpeedLimit()
{
Motor motor = new Motor();
motor.SetSpeed(10f);
Car car = new Car(null, motor);
SpeedLimit speedLimit = new SpeedLimit();
Assert.IsTrue(speedLimit.IsViolatedBy(car));
}
}
Le test fonctionne bien. SpeedLimit
a besoin d'un Car
avec un Motor
afin de faire sa chose. Cela ne l’intéresse pas CarRadio
du tout, alors j’ai fourni null pour cela.
Je me demande si un objet fournissant une fonctionnalité correcte sans être entièrement construit est une violation de SRP ou une odeur de code. J'ai l'impression que c'est le cas, mais speedLimit.IsViolatedBy(motor)
je ne me sens pas bien non plus: une limite de vitesse est violée par une voiture, pas par un moteur. Peut-être ai-je simplement besoin d'une perspective différente pour les tests unitaires par rapport au code fonctionnel, parce que l'objectif est de tester uniquement une partie du tout.
La construction d'objets avec null dans les tests unitaires a-t-elle une odeur de code?
la source
Motor
ne devrait probablement pas en avoirspeed
du tout. Il devrait avoir unthrottle
et calculer untorque
basé sur le courantrpm
etthrottle
. C’est le travail de la voitureTransmission
d’intégrer cela dans la vitesse actuelle et de transformer cela en unrpm
signal de retour vers leMotor
... Mais je suppose que vous n’avez pas eu le souci de réaliser le réalisme de toute façon, n'est-ce pas?null
radio, la limite de vitesse est correctement calculée. Maintenant, vous pouvez créer un test pour valider la limite de vitesse avec une radio; juste au cas où le comportement serait différent ...Réponses:
Dans le cas de l'exemple ci-dessus, il est raisonnable qu'un
Car
peut exister sans unCarRadio
. Dans ce cas, je dirais que non seulement il est acceptable de passer à zéroCarRadio
parfois, je dirais que c'est obligatoire. Vos tests doivent s'assurer que l'implémentation de laCar
classe est robuste et ne lève pas d'exceptions de pointeur nul lorsque aucunCarRadio
n'est présent.Cependant, prenons un exemple différent - considérons a
SteeringWheel
. Supposons qu'unCar
a doit avoir unSteeringWheel
, mais le test de vitesse ne s'en soucie pas vraiment. Dans ce cas, je ne passerais pas un zéroSteeringWheel
car cela pousserait laCar
classe dans des endroits où elle n’aurait pas été conçue. Dans ce cas, vous feriez mieux de créer une sorte deDefaultSteeringWheel
qui (pour continuer la métaphore) est bloqué en ligne droite.la source
Car
est impossible de construire unSteeringWheel
...Car
sansCarRadio
, ne serait-il pas logique de créer un constructeur qui n'en nécessite pas un et ne pas avoir à se soucier d'un null explicite ?Car
auraient jeté un NRE avec un nullCarRadio
, mais le codeSpeedLimit
correspondant ne le ferait pas. Dans le cas de la question telle que publiée, vous auriez raison et je le ferais effectivement.Oui. Notez la définition C2 de l' odeur de code : "un CodeSmell est un indice que quelque chose pourrait être faux, pas une certitude."
Si vous essayez de démontrer que
speedLimit.IsViolatedBy(car)
cela ne dépend pas de l'CarRadio
argument transmis au constructeur, il serait probablement plus clair pour le futur responsable de la maintenance de rendre cette contrainte explicite en introduisant un double test qui échoue au test s'il est appelé.Si, comme il est plus probable, vous essayez simplement de vous épargner le travail de création d’une application
CarRadio
que vous savez inutilisée dans ce cas, vous devriez remarquer queCarRadio
le test non utilisé s'infiltrer dans le test)Car
sans spécifierCarRadio
Je soupçonne fortement que le code essaie de vous dire que vous voulez un constructeur qui ressemble à
et puis ce constructeur peut décider quoi faire avec le fait qu'il n'y a pas
CarRadio
ou
ou
etc.
C'est une deuxième odeur intéressante: pour tester SpeedLimit, vous devez construire une voiture entière autour d'une radio, même si la seule chose qui intéresse le contrôle SpeedLimit est probablement la vitesse .
Cela pourrait être un indice qui
SpeedLimit.isViolatedBy
devrait accepter une interface de rôle que la voiture implémente , plutôt que d'exiger une voiture entière.IHaveSpeed
est un nom vraiment moche; Espérons que votre langue de domaine aura une amélioration.Avec cette interface en place, votre test
SpeedLimit
pourrait être beaucoup plus simple.la source
IHaveSpeed
interface de la manière (au moins en c #) de ne pas pouvoir instancier une interface elle-même.Non
Pas du tout. Au contraire, si
NULL
est une valeur qui est disponible comme argument (certaines langues ont des constructions qui DISALLOW passant d'un pointeur NULL), vous devez le tester.Une autre question est ce qui se passe lorsque vous passez
NULL
. Mais c’est exactement ce qu’est un test unitaire: s’assurer qu’un résultat défini est obtenu pour chaque entrée possible. EtNULL
est une entrée potentielle, vous devriez donc avoir un résultat défini et vous devriez avoir un test pour cela.Donc, si votre voiture est censée être constructible sans radio, vous devriez écrire un test pour cela. Si ce n'est pas le cas et est supposé lancer une exception, vous devriez écrire un test pour cela.
De toute façon, oui, vous devriez écrire un test
NULL
. Ce serait une odeur de code si vous l'omettiez. Cela signifierait que vous avez une branche de code non testée qui, comme vous venez de l’imaginer, serait exempte de bugs.Veuillez noter que je suis d’accord avec les autres réponses pour dire que votre code testé pourrait être amélioré. Néanmoins, si le code amélioré a des paramètres qui sont des pointeurs, le passage
NULL
même pour le code amélioré est un bon test. Je voudrais un test pour montrer ce qui se passe lorsque je passe enNULL
tant que moteur. Ce n'est pas une odeur de code, c'est le contraire d'une odeur de code. C'est tester.la source
Car
ici. Bien que je ne voie rien, je considérerais une déclaration erronée, mais la question concerne les testsSpeedLimit
.NULL
au constructeur deCar
.SpeedLimit
. Vous pouvez voir que le test s'appelle "SpeedLimitTest" et que la seuleAssert
vérification d'une méthode deSpeedLimit
. Les testsCar
- par exemple, "si votre voiture est supposée être constructible sans radio, vous devez écrire un test pour cela" - se produiraient dans un test différent.Personnellement, j'hésiterais à injecter des valeurs nulles. En fait, chaque fois que j'injecte des dépendances dans un constructeur, l'une des premières choses que je fais est de générer une exception ArgumentNullException si ces injections sont nulles. De cette façon, je peux les utiliser en toute sécurité dans ma classe, sans avoir recours à des dizaines de contrôles nuls répartis. Voici un modèle très commun. Notez l'utilisation de readonly pour vous assurer que les dépendances définies dans le constructeur ne peuvent pas être définies sur null (ou toute autre chose) par la suite:
Avec ce qui précède, je pourrais peut-être avoir un ou deux tests unitaires, dans lesquels j'injecte des valeurs null, juste pour m'assurer que mes contrôles des valeurs Null fonctionnent correctement.
Cela dit, cela ne pose plus aucun problème, une fois que vous faites deux choses:
Donc, pour votre exemple, vous auriez:
Plus de détails sur l'utilisation de Moq peuvent être trouvés ici .
Bien sûr, si vous voulez comprendre sans vous moquer, vous pouvez toujours le faire, du moment que vous utilisez des interfaces. Créez simplement une CarRadio et un moteur factices comme suit, puis injectez-les:
la source
IMotor
eu unegetSpeed()
méthode, leDummyMotor
aurait besoin de retourner une vitesse modifiée après un succèssetSpeed
aussi.Ce n'est pas juste d'accord, c'est une technique bien connue de rupture de dépendance pour obtenir du code hérité dans un faisceau de test. (Voir «Utilisation efficace du code hérité» par Michael Feathers.)
Ça sent? Bien...
Le test ne sent pas. Le test fait son travail. Il déclare «cet objet peut être nul et le code testé continue à fonctionner correctement».
L'odeur est dans la mise en œuvre. En déclarant un argument constructeur, vous déclarez que «cette classe a besoin de cette chose pour fonctionner correctement». Évidemment, c'est faux. Tout ce qui est faux est un peu malodorant.
la source
Revenons aux premiers principes.
Il y aura cependant des constructeurs ou des méthodes qui prendront d'autres classes en tant que paramètres. Votre façon de les gérer varie d’un cas à l’autre, mais la configuration doit être minimale, car elles sont tangentes à l’objectif. Il peut arriver que le passage d'un paramètre NULL soit parfaitement valide, par exemple une voiture sans radio.
Je suppose ici que nous testons simplement la ligne de course pour commencer (pour continuer le thème de la voiture), mais vous pouvez également passer à NULL à d’autres paramètres pour vérifier que tout code défensif et mécanisme de remise d’exception fonctionnent comme prévu. Champs obligatoires.
Jusqu'ici tout va bien. Cependant, que se passe-t-il si NULL n'est pas une valeur valide? Eh bien, vous êtes ici dans le monde trouble des moqueurs, des talons, des mannequins et des faux .
Si tout cela vous semble trop compliqué, vous utilisez déjà un mannequin en passant NULL dans le constructeur Car, car il s'agit d'une valeur qui ne sera pas potentiellement utilisée.
Ce qui devrait devenir clair assez rapidement, c’est que vous êtes potentiellement en train de manipuler votre code avec beaucoup de manipulations NULL. Si vous remplacez les paramètres de classe par des interfaces, vous ouvrez également la voie au mocking, aux DI, etc., ce qui les rend beaucoup plus simples à gérer.
la source
Jetant un coup d'oeil à votre conception, je suggérerais qu'un
Car
est composé d'un ensemble deFeatures
. Une fonctionnalité peut-être uneCarRadio
, ouEntertainmentSystem
qui peut être composée d'éléments tels qu'unCarRadio
,DvdPlayer
etc.Donc, au minimum, vous devriez construire un
Car
avec un ensemble deFeatures
.EntertainmentSystem
etCarRadio
seraient des implémenteurs de l'interface (Java, C #, etc.) depublic [I|i]nterface Feature
et ceci serait une instance du modèle de conception Composite.Par conséquent, vous construirez toujours un
Car
avecFeatures
et un test unitaire n’instanciera jamais unCar
sansFeatures
. Bien que vous puissiez envisager de vous moquer d'un ensembleBasicTestCarFeatureSet
comportant un ensemble minimal de fonctionnalités requises pour votre test le plus élémentaire.Juste quelques pensées.
John
la source