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

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

66
demandé sur kriegaex 2014-06-25 19:46:41

3 réponses

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

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

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

Un espion est une sorte d'hybride entre l'objet réel et le stub, c'est-à-dire que c'est essentiellement l'objet réel avec certaines (pas toutes) méthodes ombrées par les méthodes stub. Les méthodes non tronquées sont juste acheminé par le biais de l'objet d'origine. De cette façon, vous pouvez avoir un comportement original pour les méthodes "bon marché" ou triviales et un comportement faux 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 falsifie pas ma réponse générale:

  • un talon a pour objet de simuler un comportement spécifique. Dans Spock c'est tout un talon peut faire, donc c'est un peu la chose la plus simple.
  • un simulacre concerne la recherche d'un objet réel (peut-être coûteux), fournissant des réponses sans op pour tous les appels de méthode. À cet égard, une maquette est plus simple qu'un talon. Mais dans Spock, un simulacre peut aussi des résultats de méthode stub, c'est-à-dire être à la fois un simulacre 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 lors d'un test.
  • un espion enveloppe toujours un objet réel et par 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 des appels de méthode fonctionne également pour les espions. Dans Spock, un espion peut également modifier le comportement de l'objet original, en manipulant les paramètres d'appel de méthode et/ou les résultats ou en bloquant l'appel des méthodes originales.

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 pour 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?")
  }
}
68
répondu kriegaex 2018-01-22 11:27:44

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

Basée sur Spock docs (exemples personnalisés, mon propre libellé ajouté):

Stub: Utilisé pour faire des collaborateurs répondre aux appels de méthode, d'une certaine façon. Lorsque vous stubbing une méthode, vous ne vous souciez pas si et combien de fois la méthode va être appelée; vous voulez juste qu'elle retourne une valeur, ou effectuer un effet secondaire, chaque fois qu'elle obtient 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()
}

Une maquette peut agir comme une maquette et un talon:

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

Espion: est toujours basé sur un objet réel avec des méthodes originales qui font des choses réelles. Peut être utilisé comme un talon pour changer les valeurs de retour des méthodes select. Peut être utilisé comme une maquette pour décrire interaction.

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 talon () est un talon.
  • un simulacre() est un talon et un simulacre.
  • Un espion() est un talon, un faux et un espion.

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

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

29
répondu mikhail 2016-02-05 20:21:59

, En termes simples:

Maquette: vous Vous moquez d'un type et à la volée, vous obtenez un objet créé. Méthodes dans cet objet mock renvoie les valeurs par défaut de type de retour.

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

Espion: vous créez un objet réel, puis vous l'espionnez. Maintenant, vous peut se moquer de certaines méthodes et a choisi de ne pas le faire pour certains.

Une différence d'utilisation est vous ne pouvez pas simuler les objets de niveau de 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
répondu GKS 2014-12-02 10:56:32