web-dev-qa-db-fra.com

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

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

80
Q Liu

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

Une maquette est une classe factice qui remplace une classe réelle et renvoie un résultat nul ou nul pour chaque appel de méthode. Vous utilisez un simulacre si vous avez besoin d'une instance factice d'une classe complexe qui utiliserait sinon des ressources externes telles que des connexions réseau, des fichiers ou des bases de données, ou peut-être des dizaines d'autres objets. L'avantage des simulacres réside dans le fait que vous pouvez isoler la classe sous test du reste du système.

Un module de remplacement est également une classe factice fournissant des résultats rejoués, plus spécifiques, préparés ou préenregistrés, à certaines demandes testées. Vous pourriez dire qu'un bout est une imitation de fantaisie. Dans Spock, vous en apprendrez souvent sur les méthodes de stub.

Un espion est en quelque sorte un hybride entre objet réel et stub, c’est-à-dire qu’il s’agit en fait d’un objet réel avec certaines méthodes (pas toutes) masquées par les méthodes de stub. Les méthodes non tronquées sont simplement acheminées vers l'objet d'origine. De cette façon, vous pouvez avoir un comportement original pour des méthodes "bon marché" ou triviales et un comportement factice pour des 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 celle d'origine ci-dessus. Donc, dans le cadre de Spock, ce qu'il décrit est correct, mais cela ne falsifie pas ma réponse générale:

  • Un talon s'intéresse à la simulation d'un comportement spécifique. Dans Spock, tout ce qu'un bout peut faire, c'est donc un peu la chose la plus simple.
  • Un simulacre se soucie de remplacer un objet réel (éventuellement coûteux), en fournissant des réponses non-op pour tous les appels de méthode. À cet égard, une maquette est plus simple qu'un bout. Mais dans Spock, une simulation peut aussi produire des résultats de méthode, c’est-à-dire être à la fois une maquette et une copie. De plus, dans Spock, nous pouvons compter le nombre de fois que des méthodes fictives spécifiques avec certains paramètres ont été appelées au cours d’un test.
  • Un espion enveloppe toujours un objet réel et achemine par défaut tous les appels de méthode à 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.

Voici maintenant un exemple de test exécutable, montrant ce qui est possible et ce qui ne l’est pas. C'est un peu plus instructif que les extraits de mikhail. Merci beaucoup à 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?")
  }
}
82
kriegaex

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

Basé sur Spock docs (exemples personnalisés, ajout de ma propre formulation):

Stub: Utilisé pour que les collaborateurs répondent aux appels de méthodes d'une certaine manière. Lorsque vous modifiez 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 retourne une valeur ou qu'il produise 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 en cours de 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 simulacre peut servir de simulacre et de talon:

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 changer les valeurs de retour des méthodes sélectionnées. Peut être utilisé comme un simulacre pour décrire des 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
}

Sommaire:

  • Un stub () est un stub.
  • Une maquette () est un talon et une maquette.
  • Un espion () est un stub, un simulacre et un espion.

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

Si vous le pouvez, évitez d’utiliser Spy (). Cela pourrait être une odeur et des allusions à un test incorrect ou à une conception incorrecte de l’objet testé.

41
mikhail

En termes simples:

Mock: Vous moquez un type et à la volée, vous créez un objet. Les méthodes de cet objet factice renvoient 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 une définition en fonction de vos besoins. Ex: En méthode d'objet réel, vous appelez une api externe et renvoyez le nom d'utilisateur et l'id. Dans la méthode de l'objet stubbed, vous retournez un nom factice.

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

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

11
GKS