Zombarias, stubs e espiões no Spock Framework

O Spock fornece três ferramentas poderosas (mas diferentes em essência) que simplificam os testes de gravação: Mock, Stub e Spy.



Frequentemente, o código que precisa ser testado precisa interagir com os módulos externos chamados dependências (o artigo original usa o termo colaboradores, o que não é muito comum no ambiente em russo).


Os testes de unidade geralmente são projetados para testar uma única classe isolada usando várias variantes do mok: Mock, Stub e Spy. Portanto, os testes serão mais confiáveis ​​e menos propensos a serem interrompidos à medida que o código de dependência evoluir.


Esses testes isolados são menos propensos a problemas ao alterar os detalhes internos da implementação de dependência.


De um tradutor: toda vez que uso o Spock Framework para escrever testes, sinto que posso cometer um erro ao escolher uma maneira de substituir dependências. Este artigo possui a menor cábula possível para a escolha de um mecanismo para a criação de mokas.


TL; DR


Zombaria


Use Mock para:


  • verificações de contrato entre código de teste e dependências
  • verificando se os métodos de dependência são chamados o número correto de vezes
  • validação de parâmetros com os quais o código de dependência é chamado

Stubs


Use o Stub para:


  • fornecendo resultados de chamadas predefinidos
  • executando ações predefinidas esperadas de dependências, como lançar exceções

Espiões


Medo de espiões. Como a documentação da Spock diz:


Pense duas vezes antes de usar esse mecanismo. Talvez você deva redesenhar sua solução e reorganizar seu código.

Mas acontece que há situações em que precisamos trabalhar com código legado. O código legado pode ser difícil ou até impossível de testar com mobs e stubs. Nesse caso, existe apenas uma solução: use Spy.


É melhor ter código legado coberto com testes usando Spy do que não ter testes legados.


Use o Spy para:


  • teste de código legado que não pode ser testado por outros métodos
  • verificando se os métodos de dependência são chamados o número correto de vezes
  • validação de parâmetros passados
  • fornecendo uma resposta de dependência predefinida
  • executando ações predefinidas em resposta a chamadas para métodos de dependência

Zombaria



Todo o poder das moxas se manifesta quando a tarefa de um teste de unidade é verificar o contrato entre o código testado e as dependências. Vejamos o exemplo a seguir, onde temos um controlador FooController que usa o FooService como uma dependência e testamos essa funcionalidade com mobs.


FooController.groovy


 package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooController { FooService fooService def doSomething() { render fooService.doSomething("Sally") } } 

FooService.groovy


 package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooService { String doSomething(String name) { "Hi ${name}, FooService did something" } } 

Nesse cenário, queremos escrever um teste que irá testar:


  • contrato entre FooController e FooService
  • FooService.doSomething(name) é chamado o número correto de vezes
  • FooService.doSomething(name) é chamado com o parâmetro correto

Dê uma olhada no teste:


MockSpec.groovy


 package com.mycompany.myapp import grails.testing.web.controllers.ControllerUnitTest import spock.lang.Specification class MockSpec extends Specification implements ControllerUnitTest<FooController> { void "Mock FooService"() { given: "  " def fooService = Mock(FooService) and: "    " controller.fooService = fooService when: "  " controller.doSomething() then: "         " 1 * fooService.doSomething("Sally") and: "  ''    - 'null'" response.text == null.toString() } } 

O teste acima cria uma simulação de serviço:


 def fooService = Mock(FooService) 

O teste também verifica se FooService.doSomething(name) é chamado uma vez e o parâmetro passado para ele corresponde à string "Sally" .


 1 * fooService.doSomething("Sally") 

O código acima resolve 4 problemas importantes:


  • cria simulação para FooService
  • verifica se FooService.doSomething(String name) é chamado exatamente uma vez com o parâmetro String e o valor "Sally"
  • isola o código de teste, substituindo a implementação de dependência

Stubs


O código testado usa dependências? O objetivo do teste é garantir que o código sob teste funcione corretamente ao interagir com dependências? Os resultados das chamadas para métodos de dependência inserem valores para o código em teste?


Se o comportamento do código testado mudar, dependendo do comportamento das dependências, você precisará usar stubs (Stub).


Vamos dar uma olhada no exemplo a seguir com FooController e FooService e testar a funcionalidade do controlador usando stubs.


FooController.groovy


 package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooController { FooService fooService def doSomething() { render fooService.doSomething("Sally") } } 

FooService.groovy


 package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooService { String doSomething(String name) { "Hi ${name}, FooService did something" } } 

Código do teste:


StubSpec.groovy


 package com.mycompany.myapp import grails.testing.web.controllers.ControllerUnitTest import spock.lang.Specification class StubSpec extends Specification implements ControllerUnitTest<FooController> { void "Stub FooService"() { given: "  " def fooService = Stub(FooService) { doSomething(_) >> "Stub did something" } and: "    " controller.fooService = fooService when: "  " controller.doSomething() then: "   " // 1 * fooService.doSomething() //        response.text == "Stub did something" } } 

Você pode criar um esboço como este:


 def fooService = Stub(FooService) { doSomething(_) >> "Stub did something" } 

O código acima resolve 4 problemas importantes:


  • cria um esboço FooService
  • garante que FooService.doSomething(String name) retorne a string "Stub did something" independentemente do parâmetro passado (portanto, usamos o caractere _ )
  • isola o código testado, substituindo a implementação da dependência por um stub

Espiões


Por favor, não leia esta seção.


Não olhe.


Pule para o próximo.


Ainda está lendo? Bem, então, tudo bem, vamos lidar com o Spy.



Não use Spy. Como a documentação da Spock diz:


Pense duas vezes antes de usar esse mecanismo. Talvez você deva redesenhar sua solução e reorganizar seu código.

Nesse caso, há situações em que precisamos trabalhar com código legado. O código legado não pode ser testado usando mobs ou stubs. Nesse caso, o espião continua sendo a única opção viável.


Espiões são diferentes de mokas ou tocos, porque não funcionam como tocos.


Quando uma dependência é substituída por um mok ou stub, um objeto de teste é criado e o código-fonte real da dependência não é executado.


O espião, por outro lado, executará o código-fonte principal da dependência para a qual o espião foi criado, mas o espião permitirá que você altere o que o espião retorna e verifique as chamadas de método, assim como mokas e stubs. (Daí o nome Spy).


Vejamos o seguinte exemplo do FooController , que usa o FooService , e depois teste a funcionalidade com um espião.


FooController.groovy


 package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooController { FooService fooService def doSomething() { render fooService.doSomething("Sally") } } 

FooService.groovy


 package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooService { String doSomething(String name) { "Hi ${name}, FooService did something" } } 

Código do teste:


SpySpec.groovy


 package com.mycompany.myapp import grails.testing.web.controllers.ControllerUnitTest import spock.lang.Specification class SpySpec extends Specification implements ControllerUnitTest<FooController> { void "Spy FooService"() { given: " -" def fooService = Spy(FooService) and: "   " controller.fooService = fooService when: "  " controller.doSomething() then: "     " 1 * fooService.doSomething("Sally") >> "A Spy can modify implementation" and: '     ' response.text == "A Spy can modify implementation" } } 

Criar uma instância de espião é bastante simples:


 def fooService = Spy(FooService) 

No código acima, o espião nos permite verificar a chamada para FooService.doSomething(name) , o número de chamadas e os valores dos parâmetros. Além disso, o espião altera a implementação do método para retornar um valor diferente.


 1 * fooService.doSomething("Sally") >> "A Spy can modify implementation" 

O código acima resolve 4 problemas importantes:


  • cria uma instância de espião para o FooService
  • verifica a interação da dependência
  • verifica como o aplicativo funciona de acordo com certos resultados de chamadas de método de dependência
  • isola o código testado, substituindo a implementação da dependência por um stub

Perguntas frequentes


Qual opção usar: Mock, Stub ou Spy?


Esta é uma pergunta que muitos desenvolvedores enfrentam. Este FAQ pode ajudar se você não tiver certeza de qual abordagem usar.


P: O objetivo é testar a verificação do contrato entre o código testado e as dependências?


A: Se você respondeu Sim, use Mock


P: O objetivo do teste é garantir que o código sob teste funcione corretamente ao interagir com dependências?


A: Se você respondeu Sim, use o Stub


P: Os resultados da chamada de métodos de dependência inserem valores para o código em teste?


A: Se você respondeu Sim, use o Stub


P: Você trabalha com código legado, que é muito difícil de testar e não tem mais opções?


A: Tente usar o Spy


Exemplo de código


Você pode encontrar o código para todos os exemplos neste artigo em:


https://github.com/ddelponte/mock-stub-spy


Links úteis


Source: https://habr.com/ru/post/pt435488/


All Articles