Classe de béton moqueur - Non recommandé

11

Je viens de lire un extrait du livre "Growing Object-Oriented Software" qui explique certaines raisons pour lesquelles se moquer de la classe concrète n'est pas recommandé.

Voici un exemple de code d'un test unitaire pour la classe MusicCentre:

public class MusicCentreTest {
  @Test public void startsCdPlayerAtTimeRequested() {
    final MutableTime scheduledTime = new MutableTime();
    CdPlayer player = new CdPlayer() { 
      @Override 
      public void scheduleToStartAt(Time startTime) {
        scheduledTime.set(startTime);
      }
    }

    MusicCentre centre = new MusicCentre(player);
    centre.startMediaAt(LATER);

    assertEquals(LATER, scheduledTime.get());
  }
}

Et sa première explication:

Le problème avec cette approche est qu'elle laisse implicitement la relation entre les objets. J'espère que nous avons maintenant clairement indiqué que l'intention du développement piloté par les tests avec des objets fantômes est de découvrir les relations entre les objets. Si je sous-classe, il n'y a rien dans le code de domaine pour rendre une telle relation visible, juste des méthodes sur un objet. Cela rend plus difficile de voir si le service qui prend en charge cette relation pourrait être pertinent ailleurs et je devrai refaire l'analyse la prochaine fois que je travaillerai avec la classe.

Je ne peux pas comprendre exactement ce qu'il veut dire quand il dit:

Cela rend plus difficile de voir si le service qui prend en charge cette relation pourrait être pertinent ailleurs et je devrai refaire l'analyse la prochaine fois que je travaillerai avec la classe.

Je comprends que le service correspond à MusicCentrela méthode appelée startMediaAt.

Que veut-il dire par «ailleurs»?

L'extrait complet est ici: http://www.mockobjects.com/2007/04/test-smell-mocking-concrete-classes.html

Mik378
la source
Ajout d'un commentaire sur son blog, car je n'ai pas pu comprendre ce qu'il voulait dire à partir de ces citations.
oligofren
@oligofren C'est vraiment une grande énigme :) ...
Mik378

Réponses:

6

L'auteur de ce message encourage l'utilisation des interfaces par rapport à l'utilisation des classes membres.

It turns out that my MusicCentre object only uses the starting and stopping methods on the CdPlayer, the rest are used by some other part of the system. I'm over-specifying my MediaCentre by requiring it to talk to a CdPlayer, what it actually needs is a ScheduledDevice.

La relation qu'il redoute de découvrir plus tard est le fait que la classe MediaCentre n'a pas besoin de tout l'objet CdPlayer. Son affirmation est qu'en utilisant une interface (vraisemblablement limitée à simplement démarrer | arrêter), il est plus facile de comprendre ce qu'est réellement l'interaction.

"ailleurs" signifie simplement que d'autres objets peuvent avoir des relations également limitées, et que l'objet membre complet n'est pas requis - un sous-ensemble de la fonctionnalité enveloppée via une interface devrait être suffisant.

La revendication commence à avoir plus de sens lorsque vous explosez toutes les fonctionnalités potentielles:

  • début
  • Arrêtez
  • pause
  • record
  • ordre de lecture aléatoire
  • exemples de pistes, début de la chanson
  • exemples de pistes, échantillon aléatoire de chanson
  • fournir des informations aux médias
  • ...

Maintenant, son affirmation "J'ai juste besoin de démarrer et d'arrêter" a plus de sens. L'utilisation de l'objet membre concret au lieu d'une interface rend moins clair pour les futurs développeurs ce qui est vraiment nécessaire. L'exécution de tests unitaires à partir de MediaCentre sur toutes les autres fonctions de CdPlayer est un gaspillage d'efforts de test car ils appartiennent à l'état "indifférent". Si la Recordfonction ne fonctionnait pas dans ce cas, nous ne nous en soucions vraiment pas car elle n'est pas requise. Mais un futur responsable ne saurait pas nécessairement cela sur la base du code, tel qu'il est écrit.

En fin de compte, la prémisse de l'auteur est de n'utiliser que ce qui est nécessaire et de faire clairement comprendre aux futurs responsables ce qui était nécessaire auparavant. L'objectif est de minimiser les retouches / réanalyses du module de code lors de la maintenance ultérieure.


la source
Merci pour cette excellente réponse. Cependant, vous avez dit: "L'exécution de tests unitaires sur toutes les autres fonctions est une perte d'effort de test car elles appartiennent à l'état" ne se soucient pas "." N'est-ce pas plutôt: "Créer des simulations pour chacune des autres fonctions est un gaspillage d'effort de test car elles appartiennent à l'état" indifférent "."?
Mik378
@ Mik378 - oui, c'est exactement ce que je voulais dire, je l'ai simplement formulé différemment. Et j'ai mis à jour ma réponse pour que ce soit plus clair.
Mais je trouve que le terme "exécution de tests unitaires" prête à confusion. Cela signifierait que MusicCentre est sur le point de tester unitairement son collaborateur ... alors qu'en fait il MOCKS son collaborateur afin de tester unitairement ses propres services. Au fait, j'en comprends maintenant le sens :)
Mik378
@ Mik378 - nous disons la même chose, et j'utilise probablement une terminologie moins précise pour ce faire. Toutes mes excuses pour la confusion.
4

Cela rend plus difficile de voir si le service qui prend en charge cette relation pourrait être pertinent ailleurs et je devrai refaire l'analyse la prochaine fois que je travaillerai avec la classe.

Après y avoir beaucoup réfléchi, j'obtiens une interprétation possible de cette citation:

Le "service" cité correspond au "fait de l'ordonnancement". Cela pourrait être exprimé par une interface bien nommée et "concentrée sur un seul rôle" nommée "ScheduledDevice" ou exprimé implicitement par une implémentation de méthode concrète ne dépendant d'aucune interface.

Dans l'exemple ci-dessus, la planification est exprimée par l'ensemble de l'objet complet nommé CDPlayer. Ainsi, cela conduit toujours à une relation implicite entre MusicCentreet "fait de programmation".

Donc, si nous commençons à injecter des classes concrètes et à les moquer dans des objets de haut niveau; lorsque nous voulons tester ceux-ci, nous devons analyser chaque objet "concret" injecté pour voir s'il présente une relation spécifique que nous DEVONS MOCKER car ils sont CACHÉS (implicites). Au contraire, le codage TOUJOURS sur l'interface permet au développeur de déterminer directement quel type de relation est sur le point d'être servi par l'objet de haut niveau et donc de détecter les fonctionnalités qui doivent être moquées afin d'isoler le test unitaire.

Mik378
la source
Je pense que vous l'avez maintenant. Malheureusement, je n'ai pas reçu de notification de votre commentaire.
Steve Freeman
3

Le service que je voulais dire ici était CDPlayer.scheduleToStartAt (). C'est ce que le MediaCentre appelle - le collaborateur dont il a besoin pour fonctionner. Le MediaCentre est l'objet à tester.

L'idée est que si je précise exactement ce dont dépend le MediaCentre, et non une classe d'implémentation, je peux donner un nom à ce rôle de dépendance et en parler. Tout ce que le MediaCentre doit savoir, c'est qu'il parle à ScheduledDevices. Comme le reste du système change, je n'aurai pas besoin de modifier le MediaCentre à moins que ses fonctionnalités ne changent.

Est ce que ça aide?

Steve Freeman
la source
(auteur de cet excellent article :)) ce que je voulais interpréter était cette phrase: "Cela rend plus difficile de voir si le service qui prend en charge cette relation pourrait être pertinent ailleurs et je devrai refaire l'analyse la prochaine fois que je travaillerai avec la classe ". Quel genre d'analyse? Le fait de détecter quelle méthode d'objet est censée implémenter la relation puisque celle-ci est clairement cachée?
Mik378