Différence entre Mock / Stub / Spy dans le cadre de test Spock


101

Je ne comprends pas la différence entre les tests Mock, Stub et Spy dans les tests Spock et les didacticiels que j'ai consultés en ligne ne les expliquent pas en détail.

Réponses:


94

Attention: je vais trop simplifier et peut-être même légèrement falsifier dans les paragraphes à venir. Pour plus d'informations, consultez le site Web de Martin Fowler .

Un simulacre est une classe factice remplaçant une vraie, renvoyant quelque chose comme null ou 0 pour chaque appel de méthode. Vous utilisez une simulation si vous avez besoin d'une instance factice d'une classe complexe qui, autrement, utiliserait des ressources externes telles que des connexions réseau, des fichiers ou des bases de données ou utiliserait peut-être des dizaines d'autres objets. L'avantage des simulations est que vous pouvez isoler la classe testée du reste du système.

Un stub est également une classe factice fournissant des résultats plus spécifiques, préparés ou préenregistrés, rejoués à certaines requêtes en cours de test. On pourrait dire qu'un talon est un simulacre de fantaisie. Dans Spock, vous lirez souvent sur les méthodes de stub.

Un espion est une sorte d'hybride entre un objet réel et un stub, c'est-à-dire qu'il s'agit essentiellement de l'objet réel avec certaines méthodes (pas toutes) masquées par des méthodes de stub. Les méthodes non stubbed sont simplement acheminées vers l'objet d'origine. De cette façon, vous pouvez avoir un comportement original pour les méthodes «bon marché» ou triviales et un faux comportement pour les méthodes «coûteuses» ou complexes.


Mise à jour 2017-02-06: En fait, la réponse de l'utilisateur Mikhail est plus spécifique à Spock que ma réponse originale ci-dessus. Donc, dans le cadre de Spock, ce qu'il décrit est correct, mais cela ne fausse pas ma réponse générale:

  • Un stub consiste à simuler un comportement spécifique. Dans Spock, c'est tout ce qu'un stub peut faire, c'est donc la chose la plus simple.
  • Un simulacre consiste à remplacer un objet réel (éventuellement coûteux), fournissant des réponses sans opération pour tous les appels de méthode. À cet égard, un simulacre est plus simple qu'un talon. Mais dans Spock, une simulation peut également stub des résultats de la méthode, c'est-à-dire être à la fois une simulation et un stub. De plus, dans Spock, nous pouvons compter la fréquence à laquelle des méthodes fictives spécifiques avec certains paramètres ont été appelées pendant un test.
  • Un espion enveloppe toujours un objet réel et par défaut achemine tous les appels de méthode vers l'objet d'origine, en passant également par les résultats d'origine. Le comptage d'appels de méthode fonctionne également pour les espions. Dans Spock, un espion peut également modifier le comportement de l'objet d'origine, manipuler les paramètres d'appel de méthode et / ou les résultats ou empêcher les méthodes d'origine d'être appelées du tout.

Voici maintenant un exemple de test exécutable, démontrant ce qui est possible et ce qui ne l'est pas. C'est un peu plus instructif que les extraits de mikhail. Un grand merci à lui de m'avoir inspiré pour améliorer ma propre réponse! :-)

package de.scrum_master.stackoverflow

import org.spockframework.mock.TooFewInvocationsError
import org.spockframework.runtime.InvalidSpecException
import spock.lang.FailsWith
import spock.lang.Specification

class MockStubSpyTest extends Specification {

  static class Publisher {
    List<Subscriber> subscribers = new ArrayList<>()

    void addSubscriber(Subscriber subscriber) {
      subscribers.add(subscriber)
    }

    void send(String message) {
      for (Subscriber subscriber : subscribers)
        subscriber.receive(message);
    }
  }

  static interface Subscriber {
    String receive(String message)
  }

  static class MySubscriber implements Subscriber {
    @Override
    String receive(String message) {
      if (message ==~ /[A-Za-z ]+/)
        return "ok"
      return "uh-oh"
    }
  }

  Subscriber realSubscriber1 = new MySubscriber()
  Subscriber realSubscriber2 = new MySubscriber()
  Publisher publisher = new Publisher(subscribers: [realSubscriber1, realSubscriber2])

  def "Real objects can be tested normally"() {
    expect:
    realSubscriber1.receive("Hello subscribers") == "ok"
    realSubscriber1.receive("Anyone there?") == "uh-oh"
  }

  @FailsWith(TooFewInvocationsError)
  def "Real objects cannot have interactions"() {
    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then:
    2 * realSubscriber1.receive(_)
  }

  def "Stubs can simulate behaviour"() {
    given:
    def stubSubscriber = Stub(Subscriber) {
      receive(_) >>> ["hey", "ho"]
    }

    expect:
    stubSubscriber.receive("Hello subscribers") == "hey"
    stubSubscriber.receive("Anyone there?") == "ho"
    stubSubscriber.receive("What else?") == "ho"
  }

  @FailsWith(InvalidSpecException)
  def "Stubs cannot have interactions"() {
    given: "stubbed subscriber registered with publisher"
    def stubSubscriber = Stub(Subscriber) {
      receive(_) >> "hey"
    }
    publisher.addSubscriber(stubSubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then:
    2 * stubSubscriber.receive(_)
  }

  def "Mocks can simulate behaviour and have interactions"() {
    given:
    def mockSubscriber = Mock(Subscriber) {
      3 * receive(_) >>> ["hey", "ho"]
    }
    publisher.addSubscriber(mockSubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then: "check interactions"
    1 * mockSubscriber.receive("Hello subscribers")
    1 * mockSubscriber.receive("Anyone there?")

    and: "check behaviour exactly 3 times"
    mockSubscriber.receive("foo") == "hey"
    mockSubscriber.receive("bar") == "ho"
    mockSubscriber.receive("zot") == "ho"
  }

  def "Spies can have interactions"() {
    given:
    def spySubscriber = Spy(MySubscriber)
    publisher.addSubscriber(spySubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then: "check interactions"
    1 * spySubscriber.receive("Hello subscribers")
    1 * spySubscriber.receive("Anyone there?")

    and: "check behaviour for real object (a spy is not a mock!)"
    spySubscriber.receive("Hello subscribers") == "ok"
    spySubscriber.receive("Anyone there?") == "uh-oh"
  }

  def "Spies can modify behaviour and have interactions"() {
    given:
    def spyPublisher = Spy(Publisher) {
      send(_) >> { String message -> callRealMethodWithArgs("#" + message) }
    }
    def mockSubscriber = Mock(MySubscriber)
    spyPublisher.addSubscriber(mockSubscriber)

    when:
    spyPublisher.send("Hello subscribers")
    spyPublisher.send("Anyone there?")

    then: "check interactions"
    1 * mockSubscriber.receive("#Hello subscribers")
    1 * mockSubscriber.receive("#Anyone there?")
  }
}

La différence entre mock et stub n'est pas claire ici. Avec des simulations, on veut vérifier le comportement (si et combien de fois la méthode va être appelée). Avec les stubs, on ne vérifie que l'état (par exemple la taille de la collection après test). Pour votre information: les simulacres peuvent également fournir des résultats préparés.
chipiik

Merci @mikhail et chipiik pour vos commentaires. J'ai mis à jour ma réponse, j'espère améliorer et clarifier certaines choses que j'avais initialement écrites. Avertissement: Dans ma réponse initiale, j'ai dit que je simplifiais à l'excès et falsifiais légèrement certains faits liés à Spock. Je voulais que les gens comprennent les différences fondamentales entre le stubbing, la moquerie et l'espionnage.
kriegaex le

@chipiik, encore une chose en réponse à votre commentaire: j'accompagne des équipes de développement depuis de nombreuses années et je les ai vues utiliser Spock ou d'autres JUnit avec d'autres frameworks simulés. Dans la plupart des cas, lors de l'utilisation de simulacres, ils ne l'ont pas fait pour vérifier le comportement (c'est-à-dire pour compter les appels à la méthode) mais pour isoler le sujet testé de son environnement. Le comptage des interactions IMO n'est qu'un complément et doit être utilisé avec prudence et parcimonie, car ces tests ont tendance à se rompre lorsqu'ils testent le câblage des composants plus que leur comportement réel.
kriegaex

Sa réponse brève mais toujours très utile
Chaklader Asfak Arefe

55

La question se situait dans le contexte du cadre Spock et je ne pense pas que les réponses actuelles en tiennent compte.

Basé sur des documents Spock (exemples personnalisés, ma propre formulation a été ajoutée):

Stub: utilisé pour que les collaborateurs répondent aux appels de méthode d'une certaine manière. Lorsque vous stubbing une méthode, vous ne vous souciez pas de savoir si et combien de fois la méthode sera appelée; vous voulez juste qu'il renvoie une valeur, ou effectue un effet secondaire, chaque fois qu'il est appelé.

subscriber.receive(_) >> "ok" // subscriber is a Stub()

Mock: utilisé pour décrire les interactions entre l'objet sous spécification et ses collaborateurs.

def "should send message to subscriber"() {
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("hello") // subscriber is a Mock()
}

Un Mock peut agir comme un Mock et un Stub:

1 * subscriber.receive("message1") >> "ok" // subscriber is a Mock()

Spy: est toujours basé sur un objet réel avec des méthodes originales qui font de vraies choses. Peut être utilisé comme un stub pour modifier les valeurs de retour des méthodes de sélection. Peut être utilisé comme une maquette pour décrire les interactions.

def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])

def "should send message to subscriber"() {
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("message1") >> "ok" // subscriber is a Spy(), used as a Mock an Stub
}

def "should send message to subscriber (actually handle 'receive')"() {
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("message1") // subscriber is a Spy(), used as a Mock, uses real 'receive' function
}

Résumé:

  • Un stub () est un stub.
  • Un Mock () est un Stub and Mock.
  • Un Spy () est un Stub, Mock et Spy.

Évitez d'utiliser Mock () si Stub () est suffisant.

Évitez d'utiliser Spy () si vous le pouvez, car cela pourrait être une odeur et des indices de test incorrect ou de conception incorrecte de l'objet testé.


1
Juste pour ajouter: une autre raison pour laquelle vous souhaitez minimiser l'utilisation des simulacres est qu'une simulation est très similaire à une affirmation, en ce que vous vérifiez des éléments sur une maquette qui peuvent échouer au test, et vous voulez toujours minimiser le nombre de vérifications. vous faites dans un test, pour garder le test concentré et simple. Donc, idéalement, il ne devrait y avoir qu'une seule maquette par test.
Sammi

1
"Un espion () est un stub, un simulacre et un espion." ce n'est pas vrai pour les espions sinon?
K - La toxicité du SO augmente.

2
J'ai juste jeté un coup d'œil aux espions Sinon et ils ne semblent pas se comporter comme des Mocks ou des Stubs. Notez que cette question / réponse est dans le contexte de Spock, qui est Groovy, pas JS.
mikhail

Cela devrait être la bonne réponse, car elle est limitée au contexte Spock. De plus, dire qu'un stub est une maquette sophistiquée peut être trompeur, car une maquette a des fonctionnalités supplémentaires (vérification du nombre d'appels) que le stub n'a pas (mock> plus chic que stub). Encore une fois, moquez-vous et stubs selon Spock.
CGK

13

En termes simples:

Mock: Vous vous moquez d'un type et à la volée vous obtenez un objet créé. Les méthodes de cet objet fictif retournent les valeurs par défaut du type de retour.

Stub: vous créez une classe de stub où les méthodes sont redéfinies avec la définition selon vos besoins. Ex: Dans la méthode de l'objet réel, vous appelez une API externe et renvoyez le nom d'utilisateur et l'id. Dans la méthode d'objet stubbed, vous retournez un nom factice.

Spy: Vous créez un objet réel et ensuite vous l'espionnez. Vous pouvez maintenant vous moquer de certaines méthodes et choisir de ne pas le faire pour certaines.

Une différence d'utilisation est que vous ne pouvez pas simuler des objets de niveau méthode. alors que vous pouvez créer un objet par défaut dans method, puis l'espionner pour obtenir le comportement souhaité des méthodes dans l'objet espionné.


0

Les stubs ne servent en réalité qu'à faciliter le test unitaire, ils ne font pas partie du test. Les simulacres, font partie du test, font partie de la vérification, font partie de la réussite / échec.

Donc, disons que vous avez une méthode qui prend un objet comme paramètre. Vous ne faites jamais rien qui modifie ce paramètre dans le test. Vous en lisez simplement une valeur. C'est un bout.

Si vous changez quoi que ce soit, ou devez vérifier une sorte d'interaction avec l'objet, alors c'est une simulation.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.