Diferencia entre Mock / Stub / Spy en el marco de prueba de Spock

101

No entiendo la diferencia entre Mock, Stub y Spy in Spock testing y los tutoriales que he estado viendo en línea no los explican en detalle.

Wang-Zhao-Liu QM
fuente

Respuestas:

94

Atención: voy a simplificar demasiado y tal vez incluso falsificar ligeramente en los próximos párrafos. Para obtener información más detallada, consulte el sitio web de Martin Fowler .

Un simulacro es una clase ficticia que reemplaza a una real, devolviendo algo como nulo o 0 para cada llamada al método. Utilice un simulacro si necesita una instancia ficticia de una clase compleja que, de otro modo, usaría recursos externos como conexiones de red, archivos o bases de datos o tal vez usaría docenas de otros objetos. La ventaja de las simulaciones es que puede aislar la clase bajo prueba del resto del sistema.

Un stub es también una clase ficticia que proporciona resultados reproducidos más específicos, preparados o pregrabados para determinadas solicitudes bajo prueba. Se podría decir que un talón es una burla elegante. En Spock, a menudo leerá sobre métodos de código auxiliar.

Un espía es una especie de híbrido entre un objeto real y un código auxiliar, es decir, es básicamente el objeto real con algunos métodos (no todos) sombreados por métodos auxiliares. Los métodos no aptos se enrutan simplemente al objeto original. De esta manera, puede tener un comportamiento original para los métodos "baratos" o triviales y un comportamiento falso para los métodos "costosos" o complejos.


Actualización 2017-02-06: En realidad, la respuesta del usuario mikhail es más específica para Spock que la original anterior. Entonces, dentro del alcance de Spock, lo que describe es correcto, pero eso no falsifica mi respuesta general:

  • Un stub se ocupa de simular un comportamiento específico. En Spock esto es todo lo que puede hacer un stub, por lo que es lo más simple.
  • Un simulacro se ocupa de reemplazar un objeto real (posiblemente costoso), proporcionando respuestas no operativas para todas las llamadas a métodos. En este sentido, un simulacro es más simple que un talón. Pero en Spock, un simulacro también puede eliminar los resultados del método, es decir, ser un simulacro y un talón. Además, en Spock podemos contar con qué frecuencia se han llamado métodos simulados específicos con ciertos parámetros durante una prueba.
  • Un espía siempre envuelve un objeto real y, por defecto, enruta todas las llamadas al método al objeto original, pasando también por los resultados originales. El recuento de llamadas de método también funciona para espías. En Spock, un espía también puede modificar el comportamiento del objeto original, manipulando los parámetros de llamada al método y / o los resultados o bloqueando los métodos originales para que no sean llamados.

Ahora aquí hay una prueba de ejemplo ejecutable, que demuestra qué es posible y qué no. Es un poco más instructivo que los fragmentos de mikhail. ¡Muchas gracias a él por inspirarme a mejorar mi propia respuesta! :-)

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?")
  }
}
kriegaex
fuente
La diferencia entre simulacro y stub no está clara aquí. Con las simulaciones, se quiere verificar el comportamiento (si se va a llamar al método y cuántas veces). Con stubs, se verifica solo el estado (por ejemplo, el tamaño de la colección después de la prueba). FYI: las simulaciones también pueden proporcionar resultados preparados.
chipiik
Gracias @mikhail y chipiik por sus comentarios. He actualizado mi respuesta, con suerte mejorando y aclarando algunas cosas que escribí originalmente. Descargo de responsabilidad: en mi respuesta original dije que estaba simplificando demasiado y falsificando ligeramente algunos hechos relacionados con Spock. Quería que la gente entendiera las diferencias básicas entre taconear, burlarse y espiar.
kriegaex
@chipiik, una cosa más como respuesta a su comentario: he estado entrenando equipos de desarrollo durante muchos años y los he visto usar Spock u otro JUnit con otros marcos simulados. En la mayoría de los casos, al usar simulacros, no lo hicieron para verificar el comportamiento (es decir, contar las llamadas al método) sino para aislar al sujeto bajo prueba de su entorno. El recuento de interacciones IMO es solo una ventaja adicional y debe usarse con cuidado y moderación porque existe una tendencia a que tales pruebas se rompan cuando prueban el cableado de los componentes más que su comportamiento real.
kriegaex
Su respuesta breve pero aún muy útil
Chaklader Asfak Arefe
55

La pregunta estaba en el contexto del marco de Spock y no creo que las respuestas actuales tomen esto en cuenta.

Basado en documentos de Spock (ejemplos personalizados, mi propia redacción agregada):

Stub: se utiliza para hacer que los colaboradores respondan a las llamadas a métodos de cierta manera. Al apuntar un método, no le importa si se va a llamar al método ni cuántas veces; solo desea que devuelva algún valor o realice algún efecto secundario, siempre que se llame.

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

Simulacro: Se utiliza para describir interacciones entre el objeto bajo especificación y sus colaboradores.

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

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

Un Mock puede actuar como Mock y Stub:

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

Espía: siempre se basa en un objeto real con métodos originales que hacen cosas reales. Se puede usar como un Stub para cambiar los valores de retorno de los métodos seleccionados. Puede usarse como un simulacro para describir interacciones.

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
}

Resumen:

  • Un Stub () es un Stub.
  • Un Mock () es un Stub y un Mock.
  • Un Spy () es un Stub, Mock y Spy.

Evite usar Mock () si Stub () es suficiente.

Evite usar Spy () si puede, tener que hacerlo podría ser un olor y pistas de una prueba incorrecta o un diseño incorrecto del objeto bajo prueba.

Mikhail
fuente
1
Solo para agregar: otra razón por la que desea minimizar el uso de simulacros, es que un simulacro es muy similar a una afirmación, ya que verifica las cosas en un simulacro que pueden fallar la prueba, y siempre desea minimizar la cantidad de verificaciones haces en una prueba, para mantener la prueba enfocada y simple. Entonces, idealmente, solo debería haber una simulación por prueba.
Sammi
1
"Un Spy () es un Stub, Mock and Spy". esto no es cierto para los espías de Sinon?
K - La toxicidad en SO está aumentando.
2
Acabo de echar un vistazo rápido a los espías de Sinon y parece que no se comportan como Mocks o Stubs. Tenga en cuenta que esta pregunta / respuesta está en el contexto de Spock, que es Groovy, no JS.
Mikhail
Esta debería ser la respuesta correcta, ya que está dentro del contexto de Spock. Además, decir que un código auxiliar es un simulacro elegante puede ser engañoso, ya que un código auxiliar tiene una funcionalidad adicional (verificando el recuento de invocaciones) que el código auxiliar no tiene (simulacro> más elegante que el código auxiliar). Una vez más, burlarse y talones según Spock.
CGK
13

En lenguaje sencillo:

Mock: te burlas de un tipo y sobre la marcha obtienes un objeto creado. Los métodos de este objeto simulado devuelven los valores predeterminados del tipo de retorno.

Stub: crea una clase stub donde los métodos se redefinen con una definición según sus requisitos. Ejemplo: en el método de objeto real, llama a una api externa y devuelve el nombre de usuario contra y la identificación. En el método de objeto stubbed devuelve un nombre ficticio.

Espía: Creas un objeto real y luego lo espías. Ahora puede burlarse de algunos métodos y optar por no hacerlo para algunos.

Una diferencia de uso es que no puede simular objetos de nivel de método. mientras que puede crear un objeto predeterminado en el método y luego espiarlo para obtener el comportamiento deseado de los métodos en el objeto espiado.

GKS
fuente
0

Los stubs son en realidad solo para facilitar la prueba unitaria, no son parte de la prueba. Los simulacros son parte de la prueba, parte de la verificación, parte del pasa / no pasa.

Entonces, digamos que tiene un método que toma un objeto como parámetro. Nunca haces nada que cambie este parámetro en la prueba. Simplemente lee un valor de él. Eso es un trozo.

Si cambia algo, o necesita verificar algún tipo de interacción con el objeto, entonces es una simulación.

Más de cinco
fuente