web-dev-qa-db-fra.com

La construction d'objets avec des paramètres nuls dans les tests unitaires est-elle OK?

J'ai commencé à écrire des tests unitaires pour mon projet actuel. Mais je n'ai pas vraiment d'expérience avec ça. Je veux d'abord "l'obtenir" complètement, donc je n'utilise actuellement ni mon framework IoC ni une bibliothèque de simulation.

Je me demandais s'il n'y avait rien de mal à fournir des arguments nuls aux constructeurs d'objets dans les tests unitaires. Permettez-moi de fournir 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 (TM), réduit aux seules parties importantes de la question. J'ai maintenant écrit un test quelque chose comme 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 pour faire son travail. Il n'est pas du tout intéressé par un CarRadio, j'ai donc fourni null pour cela.

Je me demande si un objet fournissant des fonctionnalités correctes sans être entièrement construit est une violation de SRP ou une odeur de code. J'ai ce sentiment persistant que c'est le cas, mais speedLimit.IsViolatedBy(motor) ne se sent pas bien non plus - une limite de vitesse est violée par une voiture, pas par un moteur. Peut-être que j'ai juste besoin d'une perspective différente pour les tests unitaires par rapport au code de travail, car l'intention est de ne tester qu'une partie de l'ensemble.

La construction d'objets avec null dans les tests unitaires est-elle une odeur de code?

37
R. Schmitz

Dans le cas de l'exemple ci-dessus, il est raisonnable qu'un Car puisse exister sans CarRadio. Dans ce cas, je dirais que non seulement il est acceptable de passer un null CarRadio parfois, je dirais que c'est obligatoire. Vos tests doivent garantir que l'implémentation de la classe Car est robuste et ne lève pas d'exceptions de pointeur nul quand aucun CarRadio n'est présent.

Cependant, prenons un exemple différent - considérons un SteeringWheel. Supposons qu'un Car doit avoir un SteeringWheel, mais le test de vitesse ne s'en soucie pas vraiment. Dans ce cas, je ne passerais pas un SteeringWheel nul car cela pousse alors la classe Car dans des endroits où elle n'est pas conçue pour aller. Dans ce cas, vous feriez mieux de créer une sorte de DefaultSteeringWheel qui (pour continuer la métaphore) est verrouillé en ligne droite.

70
Pete

La construction d'objets avec null dans les tests unitaires est-elle une odeur de code?

Oui. Notez la définition C2 de odeur de code : "un CodeSmell est un indice que quelque chose pourrait être faux, pas une certitude."

Le test fonctionne bien. SpeedLimit a besoin d'une voiture avec un moteur pour faire son travail. Il n'est pas du tout intéressé par un CarRadio, j'ai donc fourni null pour cela.

Si vous essayez de démontrer que speedLimit.IsViolatedBy(car) ne dépend pas de l'argument CarRadio passé au constructeur, alors il serait probablement plus clair pour le futur responsable de rendre cette contrainte explicite en introduisant un test double qui échoue au test s'il est invoqué.

Si, comme cela est plus probable, vous essayez simplement de vous épargner le travail de création d'un CarRadio dont vous savez qu'il n'est pas utilisé dans ce cas, vous devriez remarquer que

  • c'est une violation de l'encapsulation (vous laissez le fait que CarRadio n'est pas utilisé s'échapper dans le test)
  • vous avez découvert au moins un cas où vous souhaitez créer un Car sans spécifier un CarRadio

Je soupçonne fortement que le code essaie de vous dire que vous voulez un constructeur qui ressemble à

public Car(IMotor motor) {...}

puis ce constructeur peut décider quoi faire avec le fait qu'il n'y a pas de CarRadio

public Car(IMotor motor) {
    this(null, motor);
}

ou

public Car(IMotor motor) {
    this( new ICarRadio() {...} , motor);
}

ou

public Car(IMotor motor) {
    this( new DefaultRadio(), motor);
}

etc.

Il s'agit de passer null à autre chose lors du test de SpeedLimit. Vous pouvez voir que le test est appelé "SpeedLimitTest" et le seul Assert vérifie une méthode de SpeedLimit.

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 que le contrôle SpeedLimit se soucie probablement est la vitesse .

Cela pourrait être un indice que SpeedLimit.isViolatedBy devrait accepter une interface de rôle que la voiture implémente , plutôt que d'exiger une voiture entière.

interface IHaveSpeed {
    float speed();
}

class Car implements IHaveSpeed {...}

IHaveSpeed est un nom vraiment nul; nous espérons que votre langue de domaine aura une amélioration.

Avec cette interface en place, votre test pour SpeedLimit pourrait être beaucoup plus simple

public void TestSpeedLimit()
{
    IHaveSpeed somethingTravelingQuickly = new IHaveSpeed {
        float speed() { return 10f; }
    }

    SpeedLimit speedLimit = new SpeedLimit();
    Assert.IsTrue(speedLimit.IsViolatedBy(somethingTravelingQuickly));
}
23
VoiceOfUnreason

La construction d'objets avec null dans les tests unitaires est-elle une odeur de code?

Non

Pas du tout. Au contraire, si NULL est une valeur disponible en argument (certaines langues peuvent avoir des constructions qui interdisent le passage d'un pointeur nul), vous devriez la tester.

Une autre question est de savoir ce qui se passe lorsque vous passez NULL. Mais c'est exactement ce qu'est un test unitaire: s'assurer qu'un est défini se produit lorsque pour chaque entrée possible. Et NULL est une entrée potentielle, donc vous devriez avoir un résultat défini et vous devriez avoir un test pour cela.

Donc, si votre voiture est supposée être constructible sans radio, vous devriez écrivez un test pour cela. Si ce n'est pas le cas et que est supposé lever une exception, vous devez écrire un test pour ça.

Dans tous les cas, oui vous devriez écrire un test pour NULL. Ce serait une odeur de code si vous l'ignoriez. Cela signifierait que vous avez une branche de code non testée que vous venez de supposer comme par magie exempte de bogues.


Veuillez noter que je suis d'accord avec les autres réponses que votre code sous test pourrait être amélioré. Néanmoins, si le code amélioré a des paramètres qui sont des pointeurs, passer NULL même pour le code amélioré est un bon test. Je voudrais qu'un test montre ce qui se passe quand je passe NULL comme moteur. Ce n'est pas une odeur de code, c'est l'opposé d'une odeur de code. C'est des tests.

18
nvoigt

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 lever une ArgumentNullException si ces injections sont nulles. De cette façon, je peux les utiliser en toute sécurité dans ma classe, sans des dizaines de contrôles nuls répartis autour. Voici un schéma très courant. Notez l'utilisation de readonly pour garantir que les dépendances définies dans le constructeur ne peuvent pas être définies sur null (ou toute autre chose) par la suite:

class Car
{
   private readonly ICarRadio _carRadio;
   private readonly IMotor _motor;

   public Car(ICarRadio carRadio, IMotor motor)
   {
      if (carRadio == null)
         throw new ArgumentNullException(nameof(carRadio));

      if (motor == null)
         throw new ArgumentNullException(nameof(motor));

      _carRadio = carRadio;
      _motor = motor;
   }
}

Avec ce qui précède, je pourrais peut-être avoir un test unitaire ou deux, où j'injecter des valeurs nulles, juste pour m'assurer que mes vérifications nulles fonctionnent comme prévu.

Cela dit, cela devient un non-problème, une fois que vous faites deux choses:

  1. Programmez sur des interfaces au lieu de classes.
  2. Utilisez un framework de simulation (comme Moq pour C #) pour injecter des dépendances simulées au lieu de null.

Donc, pour votre exemple, vous auriez:

interface ICarRadio
{
   bool Tune(float frequency);
}

interface Motor
{
   bool SetSpeed(float speed);
}

...
var car = new Car(new Moq<ICarRadio>().Object, new Moq<IMotor>().Object);

Plus de détails sur l'utilisation de Moq peuvent être trouvés ici .

Bien sûr, si vous voulez le comprendre sans vous moquer d'abord, vous pouvez toujours le faire, tant que vous utilisez des interfaces. Créez simplement un CarRadio et un moteur factices comme suit, et injectez-les à la place:

class DummyCarRadio : ICarRadio
{
   ...
}
class DummyMotor : IMotor
{
   ...
}

...
var car = new Car(new DummyCarRadio(), new DummyMotor())
7
Eternal21

Revenons en arrière aux premiers principes.

Un test unitaire teste certains aspects d'une seule classe.

Il y aura cependant des constructeurs ou des méthodes qui prendront d'autres classes comme paramètres. La façon dont vous les gérez variera d'un cas à l'autre, mais la configuration devrait être minimale, car elles sont tangentes à l'objectif. Il peut y avoir des scénarios où le passage d'un paramètre NULL est parfaitement valide - comme 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 mai voulez également passer NULL à d'autres paramètres pour vérifier que tout le code défensif et les mécanismes de gestion des exceptions fonctionnent selon les besoins.

Jusqu'ici tout va bien. Cependant, que se passe-t-il si NULL ne l'est pas une valeur valide. Eh bien, vous êtes ici dans le monde trouble de moqueries, talons, mannequins et contrefaçons .

Si tout cela semble un peu trop lourd à prendre, vous utilisez en fait déjà un mannequin en passant NULL dans le constructeur Car car c'est une valeur qui ne sera potentiellement pas utilisée.

Ce qui devrait devenir clair assez rapidement, c'est que vous pouvez potentiellement tirer votre code avec beaucoup de manipulation NULL. Si vous remplacez les paramètres de classe par des interfaces, vous ouvrez également la voie à la simulation et à la DI, ce qui les rend beaucoup plus simples à gérer.

1
Robbie Dee

Ce n'est pas seulement correct, c'est une technique bien connue pour briser les dépendances pour obtenir du code hérité dans un faisceau de test. (Voir "Travailler efficacement avec le 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é fonctionne toujours correctement".

L'odeur est dans la mise en œuvre. En déclarant un argument constructeur, vous déclarez "cette classe a besoin de cette chose pour fonctionner correctement". De toute évidence, c'est faux. Tout ce qui est faux est un peu malodorant.

1
RubberDuck

En jetant un œil à votre conception, je suggérerais qu'un Car est composé d'un ensemble de Features. Une fonctionnalité peut être un CarRadio, ou EntertainmentSystem qui peut consister en des choses comme un CarRadio, DvdPlayer etc.

Donc, au minimum, vous devrez construire un Car avec un ensemble de Features. EntertainmentSystem et CarRadio seraient des implémenteurs de l'interface (Java, C # etc) du public [I|i]nterface Feature et ce serait une instance du modèle de conception composite.

Par conséquent, vous construirez toujours un Car avec Features et un test unitaire n'instanciera jamais un Car sans Features. Bien que vous puissiez envisager de vous moquer d'un BasicTestCarFeatureSet qui possède un ensemble minimal de fonctionnalités requises pour votre test le plus élémentaire.

Juste quelques réflexions.

John

1
johnd27