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 vezesFooService.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: " "
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