Diferença entre Mock / Stub / Spy no framework de teste Spock

101

Não entendo a diferença entre Mock, Stub e Spy nos testes de Spock e os tutoriais que tenho visto online não os explicam em detalhes.

Wang-Zhao-Liu QM
fonte

Respostas:

94

Atenção: vou simplificar demais e talvez até falsificar um pouco nos próximos parágrafos. Para obter informações mais detalhadas, consulte o site de Martin Fowler .

Um mock é uma classe fictícia que substitui uma classe real, retornando algo como nulo ou 0 para cada chamada de método. Você usa uma simulação se precisar de uma instância simulada de uma classe complexa que, de outra forma, usaria recursos externos como conexões de rede, arquivos ou bancos de dados ou talvez dezenas de outros objetos. A vantagem dos mocks é que você pode isolar a classe em teste do resto do sistema.

Um stub também é uma classe fictícia que fornece alguns resultados reproduzidos mais específicos, preparados ou pré-gravados, para certas solicitações em teste. Você poderia dizer que um esboço é uma simulação chique. Em Spock, você frequentemente lerá sobre métodos de stub.

Um espião é uma espécie de híbrido entre objeto real e stub, ou seja, é basicamente o objeto real com alguns (não todos) métodos ocultados por métodos stub. Os métodos sem stub são apenas roteados para o objeto original. Dessa forma, você pode ter um comportamento original para métodos "baratos" ou triviais e um comportamento falso para métodos "caros" ou complexos.


Atualização 06/02/2017: Na verdade, a resposta do usuário mikhail é mais específica para Spock do que a original acima. Portanto, no âmbito de Spock, o que ele descreve está correto, mas isso não falsifica minha resposta geral:

  • Um stub se preocupa com a simulação de um comportamento específico. Em Spock, isso é tudo que um esboço pode fazer, então é a coisa mais simples.
  • Um mock está preocupado em substituir um objeto real (possivelmente caro), fornecendo respostas autônomas para todas as chamadas de método. Nesse sentido, uma simulação é mais simples do que um esboço. Mas em Spock, um mock também pode criar um esboço de resultados de método, ou seja, ser tanto um esboço quanto um esboço. Além disso, no Spock podemos contar quantas vezes métodos simulados específicos com certos parâmetros foram chamados durante um teste.
  • Um espião sempre envolve um objeto real e, por padrão, roteia todas as chamadas de método para o objeto original, passando também pelos resultados originais. A contagem de chamadas de método também funciona para espiões. No Spock, um espião também pode modificar o comportamento do objeto original, manipulando parâmetros de chamada de método e / ou resultados ou bloqueando os métodos originais de serem chamados.

Agora, aqui está um exemplo de teste executável, demonstrando o que é possível e o que não é. É um pouco mais instrutivo do que os fragmentos de Mikhail. Muito obrigado a ele por me inspirar a melhorar minha própria resposta! :-)

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
fonte
A diferença entre mock e stub não está clara aqui. Com mocks, deseja-se verificar o comportamento (se e quantas vezes o método será chamado). Com os stubs, verifica-se apenas o estado (por exemplo, tamanho da coleção após o teste). FYI: mocks podem fornecer resultados preparados também.
chipiik
Obrigado @mikhail e chipiik por seus comentários. Eu atualizei minha resposta, espero melhorar e esclarecer algumas coisas que escrevi originalmente. Isenção de responsabilidade: Em minha resposta original, eu disse que estava simplificando demais e falsificando um pouco alguns fatos relacionados a Spock. Eu queria que as pessoas entendessem as diferenças básicas entre stubbing, zombing e spying.
kriegaex
@chipiik, mais uma coisa em resposta ao seu comentário: Tenho treinado equipes de desenvolvimento por muitos anos e os vi usar Spock ou outro JUnit com outros frameworks mock. Na maioria dos casos, ao usar mocks, eles não o faziam para verificar o comportamento (isto é, contar chamadas de método), mas para isolar o sujeito sob teste de seu ambiente. O IMO de contagem de interação é apenas um acessório adicional e deve ser usado com cautela e com moderação, pois há uma tendência de que esses testes sejam interrompidos quando testam a fiação de componentes mais do que seu comportamento real.
kriegaex
Sua resposta breve, mas muito útil
Chaklader Asfak Arefe,
55

A pergunta estava no contexto da estrutura de Spock e não acredito que as respostas atuais levem isso em consideração.

Com base nos documentos do Spock (exemplos personalizados, minha própria redação adicionada):

Stub: usado para fazer os colaboradores responderem às chamadas de método de uma determinada maneira. Ao fazer o stub de um método, você não se importa se e quantas vezes o método será chamado; você só quer que ele retorne algum valor, ou execute algum efeito colateral, sempre que for chamado.

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

Mock: Usado para descrever as interações entre o objeto sob especificação e seus colaboradores.

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

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

Um Mock pode atuar como um Mock e um Stub:

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

Spy: É sempre baseado em um objeto real com métodos originais que fazem coisas reais. Pode ser usado como um Stub para alterar os valores de retorno dos métodos selecionados. Pode ser usado como um mock para descrever interações.

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
}

Resumo:

  • Um Stub () é um Stub.
  • Um Mock () é um Stub e um Mock.
  • Um Spy () é um Stub, Mock e Spy.

Evite usar Mock () se Stub () for suficiente.

Evite usar Spy () se puder; fazer isso pode ser um cheiro e dicas de teste incorreto ou design incorreto do objeto em teste.

Mikhail
fonte
1
Só para acrescentar: Outra razão pela qual você deseja minimizar o uso de simulações, é que uma simulação é muito semelhante a uma afirmação, no sentido de que você verifica as coisas em uma simulação que pode falhar no teste, e você sempre deseja minimizar a quantidade de verificações você faz em um teste, para mantê-lo focado e simples. Portanto, o ideal é que haja apenas uma simulação por teste.
Sammi
1
"Um Spy () é um Stub, Mock e Spy." isso não é verdade para espiões sinon?
K - A toxicidade em SO está crescendo.
2
Acabei de dar uma olhada rápida nos espiões da Sinon e eles parecem que não se comportam como Mocks ou Stubs. Observe que esta pergunta / resposta está no contexto de Spock, que é Groovy, não JS.
Mikhail de
Esta deve ser a resposta correta, pois o escopo é para o contexto de Spock. Além disso, dizer que um stub é um mock sofisticado pode ser enganoso, pois um mock tem uma funcionalidade extra (verificação da contagem de invocação) que o stub não possui (mock> mais sofisticado que o stub). De novo, zombe e faça stubs de acordo com Spock.
CGK
13

Em termos simples:

Simulação: Você zomba de um tipo e rapidamente cria um objeto. Os métodos neste objeto simulado retornam os valores padrão do tipo de retorno.

Stub: você cria uma classe de stub onde os métodos são redefinidos com definição de acordo com sua necessidade. Ex: No método de objeto real você chama uma API externa e retorna o nome de usuário contra e id. No método de objeto stubbed, você retorna algum nome fictício.

Espião: Você cria um objeto real e então o espia. Agora você pode simular alguns métodos e optar por não fazer isso para alguns.

Uma diferença de uso é que você não pode simular objetos de nível de método. enquanto você pode criar um objeto padrão no método e então espioná-lo para obter o comportamento desejado dos métodos no objeto espiado.

GKS
fonte
0

Os stubs são apenas para facilitar o teste de unidade, eles não fazem parte do teste. Mocks, são parte do teste, parte da verificação, parte da aprovação / reprovação.

Então, digamos que você tenha um método que recebe um objeto como parâmetro. Você nunca faz nada que altere este parâmetro no teste. Você simplesmente lê um valor dele. Isso é um esboço.

Se você mudar alguma coisa, ou precisar verificar algum tipo de interação com o objeto, então é uma simulação.

Mais de cinco
fonte