Diferença entre Stub, Mock e Spy no Spock Framework
1. Visão geral
Neste tutorial,we’re going to discuss the differences between Mock, Stub, and Spy in the Spock framework. Vamos ilustrar o que a estrutura oferece em relação aos testes baseados em interação.
Spock é uma estrutura de teste paraJavaeGroovy que ajuda a automatizar o processo de teste manual do aplicativo de software. Ele apresenta suas próprias zombarias, stubs e espiões, e vem com recursos internos para testes que normalmente exigem bibliotecas adicionais.
Primeiro, vamos ilustrar quando devemos usar stubs. Então, vamos passar por zombarias. No final, descreveremos o recentemente introduzidoSpy.
2. Dependências do Maven
Antes de começar, vamos adicionar nossoMaven dependencies:
org.spockframework
spock-core
1.3-RC1-groovy-2.5
test
org.codehaus.groovy
groovy-all
2.4.7
test
Observe que precisaremos da versão1.3-RC1-groovy-2.5 de Spock. Spy será introduzido na próxima versão estável do Spock Framework. Right now Spy is available in the first release candidate for version 1.3.
Para uma recapitulação da estrutura básica de um teste de Spock, verifique nossointroductory article on testing with Groovy and Spock.
3. Teste Baseado em Interação
Interaction-based testing is a technique that helps us test the behavior of objects - especificamente, como eles interagem uns com os outros. Para isso, podemos usar implementações fictícias chamadas zombarias e stubs.
Certamente, certamente poderíamos muito facilmente escrever nossas próprias implementações de zombarias e stubs. O problema aparece quando aumenta a quantidade do nosso código de produção. Escrever e manter esse código manualmente torna-se difícil. É por isso que usamos estruturas de simulação, que fornecem uma maneira concisa de descrever brevemente as interações esperadas. Spock has built-in support for mocking, stubbing, and spying.
Como a maioria das bibliotecas Java, Spock usaJDK dynamic proxy para interfaces de simulação eByte Buddy ou proxiescglib para classes de simulação. Ele cria implementações simuladas em tempo de execução.
Java já tem muitas bibliotecas diferentes e maduras para zombar de classes e interfaces. Embora cada um deles possa ser usado em Spock,, ainda há uma razão principal pela qual devemos usar simulações, tocos e espiões de Spock. Apresentando tudo isso a Spock,we can leverage all of Groovy’s capabilities para tornar nossos testes mais legíveis, fáceis de escrever e definitivamente mais divertidos!
4. Chamadas do método Stubbing
Às vezes,in unit tests, we need to provide a dummy behavior of the class. Pode ser um cliente para um serviço externo ou uma classe que fornece acesso ao banco de dados. Essa técnica é conhecida como stubbing.
Dependência deA stub is a controllable replacement of an existing class em nosso código testado. Isso é útil para fazer uma chamada de método que responda de uma certa maneira. Quando usamos stub, não nos importamos quantas vezes um método será chamado. Em vez disso, queremos apenas dizer: retorne esse valor quando chamado com esses dados.
Vamos passar para o código de exemplo com lógica de negócios.
4.1. Código em teste
Vamos criar uma classe de modelo chamadaItem:
public class Item {
private final String id;
private final String name;
// standard constructor, getters, equals
}
Precisamos substituir o métodoequals(Object other) para fazer nossas asserções funcionarem. Spock will use equals during assertions when we use the double equal sign (==):
new Item('1', 'name') == new Item('1', 'name')
Agora, vamos criar uma interfaceItemProvider com um método:
public interface ItemProvider {
List- getItems(List
itemIds);
}
Precisamos também de uma classe que será testada. Vamos adicionar umItemProvider as uma dependência emItemService:
public class ItemService {
private final ItemProvider itemProvider;
public ItemService(ItemProvider itemProvider) {
this.itemProvider = itemProvider;
}
List- getAllItemsSortedByName(List
itemIds) {
List- items = itemProvider.getItems(itemIds);
return items.stream()
.sorted(Comparator.comparing(Item::getName))
.collect(Collectors.toList());
}
}
We want our code to depend on an abstraction, rather than a specific implementation. É por isso que usamos uma interface. Isso pode ter muitas implementações diferentes. Por exemplo, poderíamos ler itens de um arquivo, criar um cliente HTTP para um serviço externo ou ler os dados de um banco de dados.
Neste código,we’ll need to stub the external dependency, because we only want to test our logic contained in the getAllItemsSortedByName method.
4.2. Usando um objeto em stub no código em teste
Vamos inicializar o objetoItemService no métodosetup() usando umStub para a dependênciaItemProvider:
ItemProvider itemProvider
ItemService itemService
def setup() {
itemProvider = Stub(ItemProvider)
itemService = new ItemService(itemProvider)
}
Agora,let’s make itemProvider return a list of items on every invocation with the specific argument:
itemProvider.getItems(['offer-id', 'offer-id-2']) >>
[new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]
We use >> operand to stub the method. The getItems method will always return a list of two items when called with[‘offer-id', ‘offer-id-2']list.[] é umGroovy shortcut para criar listas.
Aqui está todo o método de teste:
def 'should return items sorted by name'() {
given:
def ids = ['offer-id', 'offer-id-2']
itemProvider.getItems(ids) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]
when:
List- items = itemService.getAllItemsSortedByName(ids)
then:
items.collect { it.name } == ['Aname', 'Zname']
}
Existem muitos recursos de stub que podemos usar, como: usar restrições de correspondência de argumentos, usar sequências de valores em stubs, definir comportamentos diferentes em determinadas condições e encadear respostas de métodos.
5. Métodos de classe de zombaria
Agora, vamos falar sobre classes ou interfaces de simulação em Spock.
Às vezes,we would like to know if some method of the dependent object was called with specified arguments. Queremos nos concentrar no comportamento dos objetos e explorar como eles interagem observando as chamadas de método. Mocking é uma descrição da interação obrigatória entre os objetos na classe de teste.
Vamos testar as interações no código de exemplo que descrevemos abaixo.
5.1. Código com interação
Para um exemplo simples, vamos salvar itens no banco de dados. Após o sucesso, queremos publicar um evento no broker de mensagens sobre novos itens em nosso sistema.
O corretor de mensagens de exemplo é um RabbitMQ ou Kafka, so, geralmente, vamos apenas descrever nosso contrato:
public interface EventPublisher {
void publish(String addedOfferId);
}
Nosso método de teste salva itens não vazios no banco de dados e depois publica o evento. Salvar um item no banco de dados é irrelevante em nosso exemplo, então vamos apenas colocar um comentário:
void saveItems(List itemIds) {
List notEmptyOfferIds = itemIds.stream()
.filter(itemId -> !itemId.isEmpty())
.collect(Collectors.toList());
// save in database
notEmptyOfferIds.forEach(eventPublisher::publish);
}
5.2. Verificando a interação com objetos simulados
Agora, vamos testar a interação em nosso código.
Primeiro,we need to mock EventPublisher in our setup() method. Então, basicamente, criamos um novo campo de instância e o simulamos usando a funçãoMock(Class):
class ItemServiceTest extends Specification {
ItemProvider itemProvider
ItemService itemService
EventPublisher eventPublisher
def setup() {
itemProvider = Stub(ItemProvider)
eventPublisher = Mock(EventPublisher)
itemService = new ItemService(itemProvider, eventPublisher)
}
Agora, podemos escrever nosso método de teste. Vamos passar 3 Strings: ”, 'a', 'b' e esperamos que nossoeventPublisher publique 2 eventos com 'a' e 'b' Strings:
def 'should publish events about new non-empty saved offers'() {
given:
def offerIds = ['', 'a', 'b']
when:
itemService.saveItems(offerIds)
then:
1 * eventPublisher.publish('a')
1 * eventPublisher.publish('b')
}
Vamos dar uma olhada em nossa afirmação na seção finalthen:
1 * eventPublisher.publish('a')
Esperamos queitemService chame umeventPublisher.publish(String) com 'a' como argumento.
No esboço, falamos sobre as restrições de argumento. As mesmas regras se aplicam às zombarias. We can verify that eventPublisher.publish(String) was called twice with any non-null and non-empty argument:
2 * eventPublisher.publish({ it != null && !it.isEmpty() })
5.3. Combinando zombaria e stubbing
EmSpock,a Mock may behave the same as a Stub. Portanto, podemos dizer aos objetos simulados que, para uma determinada chamada de método, ele deve retornar os dados fornecidos.
Vamos substituir umItemProvider comMock(Class) areia para criar um novoItemService:
given:
itemProvider = Mock(ItemProvider)
itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]
itemService = new ItemService(itemProvider, eventPublisher)
when:
def items = itemService.getAllItemsSortedByName(['item-id'])
then:
items == [new Item('item-id', 'name')]
Podemos reescrever o stub da seçãogiven:
1 * itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]
Portanto, geralmente, esta linha diz:itemProvider.getItems will be called once with [‘item-‘id'] argument and return given array.
Já sabemos que as zombarias podem se comportar da mesma maneira que os stubs. Todas as regras relacionadas a restrições de argumento, retorno de vários valores e efeitos colaterais também se aplicam aMock.
6. Aulas de espionagem em Spock
Spies provide the ability to wrap an existing object. Isso significa que podemos ouvir a conversa entre o chamador e o objeto real, mas manter o comportamento do objeto original. Basicamente,Spy delegates method calls to the original object.
Em contraste comMock andStub, não podemos criar uma interfaceSpy filho. Ele envolve um objeto real; portanto, adicionalmente, precisaremos passar argumentos para o construtor. Caso contrário, o construtor padrão do tipo será invocado.
6.1. Código em teste
Vamos criar uma implementação simples paraEventPublisher. LoggingEventPublisher imprimir no console o id de cada item adicionado. Aqui está a implementação do método de interface:
@Override
public void publish(String addedOfferId) {
System.out.println("I've published: " + addedOfferId);
}
6.2. Testando comSpy
We create spies similarly to mocks and stubs, by using the Spy(Class) method.LoggingEventPublisher não tem nenhuma outra dependência de classe, então não temos que passar argumentos do construtor:
eventPublisher = Spy(LoggingEventPublisher)
Agora, vamos testar nosso espião. Precisamos de uma nova instância deItemService com nosso objeto espiado:
given:
eventPublisher = Spy(LoggingEventPublisher)
itemService = new ItemService(itemProvider, eventPublisher)
when:
itemService.saveItems(['item-id'])
then:
1 * eventPublisher.publish('item-id')
Verificamos que o métodoeventPublisher.publish foi chamado apenas uma vez. Additionally, the method call was passed to the real object, so we’ll see the output of println in the console:
I've published: item-id
Observe que quando usamos stub em um método deSpy, ele não chamará o método do objeto real. Generally, we should avoid using spies. Se tivermos que fazer isso, talvez devêssemos reorganizar o código sob as especificações.
7. Bons testes de unidade
Vamos terminar com um rápido resumo de como o uso de objetos simulados melhora nossos testes:
-
criamos conjuntos de testes determinísticos
-
não teremos efeitos colaterais
-
nossos testes de unidade serão muito rápidos
-
podemos nos concentrar na lógica contida em uma única classe Java
-
nossos testes são independentes do ambiente
8. Conclusão
Neste artigo, descrevemos completamente espiões, simulações e stubs em Groovy.Knowledge on this subject will make our tests faster, more reliable, and easier to read.
A implementação de todos os nossos exemplos pode ser encontrada emthe Github project.