Differenza tra Mock / Stub / Spy nel framework di test Spock


Risposte:


94

Attenzione: ho intenzione di semplificare eccessivamente e forse anche di falsificare leggermente nei prossimi paragrafi. Per informazioni più dettagliate vedere il sito Web di Martin Fowler .

Un mock è una classe fittizia che sostituisce una classe reale, restituendo qualcosa come null o 0 per ogni chiamata al metodo. Si utilizza un mock se è necessaria un'istanza fittizia di una classe complessa che altrimenti utilizzerebbe risorse esterne come connessioni di rete, file o database o magari userebbe dozzine di altri oggetti. Il vantaggio dei mock è che puoi isolare la classe sottoposta a test dal resto del sistema.

Uno stub è anche una classe fittizia che fornisce alcuni risultati più specifici, preparati o preregistrati e riprodotti a determinate richieste sotto test. Si potrebbe dire che uno stub è una finta fantasia. In Spock leggerai spesso dei metodi stub.

Una spia è una specie di ibrido tra oggetto reale e stub, ovvero è fondamentalmente l'oggetto reale con alcuni (non tutti) metodi oscurati dai metodi stub. I metodi non stubbed vengono semplicemente instradati all'oggetto originale. In questo modo puoi avere un comportamento originale per metodi "economici" o banali e un comportamento falso per metodi "costosi" o complessi.


Aggiornamento 2017-02-06: In realtà la risposta dell'utente mikhail è più specifica per Spock rispetto alla mia originale sopra. Quindi, nell'ambito di Spock, ciò che descrive è corretto, ma ciò non falsifica la mia risposta generale:

  • Uno stub si occupa di simulare un comportamento specifico. In Spock questo è tutto ciò che uno stub può fare, quindi è la cosa più semplice.
  • Un mock si occupa di sostituire un oggetto reale (possibilmente costoso), fornendo risposte non operative per tutte le chiamate ai metodi. A questo proposito, una simulazione è più semplice di uno stub. Ma in Spock, un mock può anche stubare i risultati del metodo, cioè essere sia un mock che uno stub. Inoltre, in Spock possiamo contare quante volte durante un test sono stati chiamati metodi mock specifici con determinati parametri.
  • Una spia avvolge sempre un oggetto reale e di default instrada tutte le chiamate di metodo all'oggetto originale, passando anche attraverso i risultati originali. Il conteggio delle chiamate al metodo funziona anche per le spie. In Spock, una spia può anche modificare il comportamento dell'oggetto originale, manipolando i parametri della chiamata al metodo e / oi risultati o bloccando del tutto la chiamata ai metodi originali.

Ora ecco un esempio di test eseguibile, che dimostra cosa è possibile e cosa no. È un po 'più istruttivo degli snippet di Mikhail. Molte grazie a lui per avermi ispirato a migliorare la mia risposta! :-)

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 differenza tra mock e stub non è chiara qui. Con i mock, si vuole verificare il comportamento (se e quante volte il metodo verrà chiamato). Con gli stub, si verifica solo lo stato (ad esempio la dimensione della raccolta dopo il test). Cordiali saluti: anche i mock possono fornire risultati preparati.
chipiik

Grazie @mikhail e chipiik per il tuo feedback. Ho aggiornato la mia risposta, sperando di migliorare e chiarire alcune cose che avevo scritto originariamente. Dichiarazione di non responsabilità: nella mia risposta originale ho detto che stavo semplificando eccessivamente e falsificando leggermente alcuni fatti relativi a Spock. Volevo che le persone capissero le differenze fondamentali tra lo stubbing, lo scherno e lo spionaggio.
kriegaex

@chipiik, ancora una cosa come risposta al tuo commento: ho istruito i team di sviluppo per molti anni e li ho visti usare Spock o altre JUnit con altri framework fittizi. Nella maggior parte dei casi, quando si usano i mock, non lo hanno fatto per verificare il comportamento (cioè contare le chiamate ai metodi) ma per isolare il soggetto sotto test dal suo ambiente. Il conteggio delle interazioni IMO è solo un accessorio aggiuntivo e dovrebbe essere usato con attenzione e con parsimonia perché c'è la tendenza che tali test si interrompano quando testano il cablaggio dei componenti più del loro comportamento effettivo.
kriegaex

La sua risposta breve ma comunque molto utile
Chaklader Asfak Arefe

55

La domanda era nel contesto del framework Spock e non credo che le risposte attuali ne tengano conto.

Basato su documenti di Spock (esempi personalizzati, aggiunta la mia formulazione):

Stub: utilizzato per fare in modo che i collaboratori rispondano alle chiamate di metodo in un certo modo. Quando si esegue lo stubbing di un metodo, non ti interessa se e quante volte il metodo verrà chiamato; vuoi solo che restituisca un valore o che esegua un effetto collaterale, ogni volta che viene chiamato.

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

Mock: utilizzato per descrivere le interazioni tra l'oggetto sotto specifica e i suoi collaboratori.

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

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

Un Mock può agire come un Mock e uno Stub:

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

Spia: si basa sempre su un oggetto reale con metodi originali che fanno cose reali. Può essere utilizzato come uno stub per modificare i valori di ritorno dei metodi di selezione. Può essere usato come un Mock per descrivere le interazioni.

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
}

Sommario:

  • Uno Stub () è uno Stub.
  • Un Mock () è uno Stub e Mock.
  • Una spia () è uno stub, una simulazione e una spia.

Evita di usare Mock () se Stub () è sufficiente.

Evita di usare Spy () se puoi, doverlo fare potrebbe essere un odore e accenni a test errati o design errato dell'oggetto sotto test.


1
Solo per aggiungere: un altro motivo per cui vuoi ridurre al minimo l'uso di mock, è che un mock è molto simile a un assert, in quanto controlli le cose su un mock che potrebbe non superare il test e vuoi sempre ridurre al minimo la quantità di assegni fai in un test, per mantenerlo concentrato e semplice. Quindi idealmente dovrebbe esserci un solo mock per test.
Sammi

1
"Una spia () è uno stub, una simulazione e una spia." questo non è vero per le spie Sinon?
K - La tossicità in SO sta crescendo.

2
Ho appena dato una rapida occhiata alle spie Sinon e sembra che non si comportino come Mock o Stub. Nota che questa domanda / risposta è nel contesto di Spock, che è Groovy, non JS.
mikhail

Questa dovrebbe essere la risposta corretta, poiché è limitata al contesto di Spock. Inoltre, dire che uno stub è una finta fantasia potrebbe essere fuorviante, poiché una finta ha funzionalità extra (controllo del conteggio delle chiamate) che lo stub non ha (finto> più elaborato dello stub). Di nuovo, beffa e muta come per Spock.
CGK

13

In parole povere:

Mock: prendi in giro un tipo e al volo ottieni un oggetto creato. I metodi in questo oggetto fittizio restituiscono i valori predefiniti del tipo restituito.

Stub: crei una classe stub in cui i metodi vengono ridefiniti con la definizione secondo le tue esigenze. Es: nel metodo dell'oggetto reale si chiama un'API esterna e si restituisce il nome utente contro e id. Nel metodo dell'oggetto stubbed si restituisce un nome fittizio.

Spia: crei un oggetto reale e poi lo spii. Ora puoi deridere alcuni metodi e scegliere di non farlo per alcuni.

Una differenza di utilizzo è che non puoi simulare oggetti a livello di metodo. mentre è possibile creare un oggetto predefinito in metodo e quindi spiarlo per ottenere il comportamento desiderato dei metodi nell'oggetto spiato.


0

Gli stub servono solo per facilitare lo unit test, non fanno parte del test. I mock, fanno parte del test, parte della verifica, parte del pass / fail.

Quindi, supponiamo di avere un metodo che accetta un oggetto come parametro. Non fai mai nulla che modifichi questo parametro nel test. Leggi semplicemente un valore da esso. Questo è uno stub.

Se modifichi qualcosa o devi verificare una sorta di interazione con l'oggetto, è una finta.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.