Diferencia entre Stub, Mock y Spy en Spock Framework

1. Información general

En este tutorial, vamos a discutir las diferencias entre Mock , Stub y Spy en el marco de Spock . Ilustraremos lo que ofrece el marco en relación con las pruebas basadas en interacción.

Spock es un marco de prueba para Java y Groovy que ayuda a automatizar el proceso de prueba manual de la aplicación de software. Presenta sus propios simulacros, stubs y espías, y viene con capacidades integradas para pruebas que normalmente requieren bibliotecas adicionales.

Primero, ilustraremos cuándo debemos usar stubs. Luego, pasaremos por las burlas. Al final, describiremos al Spy recientemente presentado .

2. Dependencias de Maven

Antes de comenzar, agreguemos nuestras dependencias de Maven:

 org.spockframework spock-core 1.3-RC1-groovy-2.5 test   org.codehaus.groovy groovy-all 2.4.7 test 

Tenga en cuenta que necesitaremos la versión 1.3-RC1-groovy-2.5 de Spock. Spy se introducirá en la próxima versión estable de Spock Framework. Ahora mismo, Spy está disponible en la primera versión candidata para la versión 1.3.

Para obtener un resumen de la estructura básica de una prueba de Spock, consulte nuestro artículo introductorio sobre las pruebas con Groovy y Spock.

3. Pruebas basadas en interacciones

La prueba basada en interacciones es una técnica que nos ayuda a probar el comportamiento de los objetos , específicamente, cómo interactúan entre sí. Para esto, podemos usar implementaciones ficticias llamadas simulacros y stubs.

Por supuesto, podríamos escribir muy fácilmente nuestras propias implementaciones de simulacros y stubs. El problema aparece cuando crece la cantidad de nuestro código de producción. Escribir y mantener este código a mano se vuelve difícil. Es por eso que usamos marcos de simulación, que brindan una forma concisa de describir brevemente las interacciones esperadas. Spock tiene soporte incorporado para burlarse, apuñalar y espiar.

Como la mayoría de las bibliotecas de Java, Spock usa un proxy dinámico JDK para simular interfaces y Byte Buddy o proxies cglib para simular clases. Crea implementaciones simuladas en tiempo de ejecución.

Java ya tiene muchas bibliotecas diferentes y maduras para burlarse de clases e interfaces. Aunque cada uno de estos se puede usar en Spock , todavía hay una razón principal por la que deberíamos usar simulacros, stubs y espías de Spock. Al presentar todo esto a Spock, podemos aprovechar todas las capacidades de Groovy para hacer que nuestras pruebas sean más legibles, más fáciles de escribir y, definitivamente, ¡más divertidas!

4. Llamadas al método de stubbing

A veces, en las pruebas unitarias, necesitamos proporcionar un comportamiento ficticio de la clase . Puede ser un cliente para un servicio externo o una clase que proporciona acceso a la base de datos. Esta técnica se conoce como stubbing.

Un stub es un reemplazo controlable de una dependencia de clase existente en nuestro código probado. Esto es útil para realizar una llamada a un método que responde de cierta manera. Cuando usamos stub, no nos importa cuántas veces se invocará un método. En su lugar, solo queremos decir: devuelve este valor cuando se llama con estos datos.

Pasemos al código de ejemplo con lógica empresarial.

4.1. Código bajo prueba

Creemos una clase modelo llamada Item :

public class Item { private final String id; private final String name; // standard constructor, getters, equals }

Necesitamos anular el método equals (Object other) para que nuestras afirmaciones funcionen. Spock usará iguales durante las afirmaciones cuando usemos el doble signo igual (==):

new Item('1', 'name') == new Item('1', 'name')

Ahora, creemos una interfaz ItemProvider con un método:

public interface ItemProvider { List getItems(List itemIds); }

También necesitaremos una clase que se probará. Agregaremos un ItemProvider como dependencia en ItemService:

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()); } }

Queremos que nuestro código dependa de una abstracción, en lugar de una implementación específica. Por eso usamos una interfaz. Esto puede tener muchas implementaciones diferentes. Por ejemplo, podríamos leer elementos de un archivo, crear un cliente HTTP para un servicio externo o leer los datos de una base de datos.

En este código, tendremos que eliminar la dependencia externa, porque solo queremos probar nuestra lógica contenida en el método getAllItemsSortedByName .

4.2. Uso de un objeto protegido en el código bajo prueba

Inicialicemos el objeto ItemService en el método setup () usando un Stub para la dependencia ItemProvider :

ItemProvider itemProvider ItemService itemService def setup() { itemProvider = Stub(ItemProvider) itemService = new ItemService(itemProvider) }

Ahora, hagamos que itemProvider devuelva una lista de elementos en cada invocación con el argumento específico :

itemProvider.getItems(['offer-id', 'offer-id-2']) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]

Usamos >> operando para stub del método. El método getItems siempre devolverá una lista de dos elementos cuando se llame con ['offer-id', 'offer-id-2'] list. [] es un atajo de Groovy para crear listas.

Aquí está todo el método de prueba:

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'] }

Hay muchas más capacidades de stubbing que podemos usar, como: usar restricciones de coincidencia de argumentos, usar secuencias de valores en stubs, definir comportamientos diferentes en ciertas condiciones y encadenar respuestas de métodos.

5. Métodos de burla de clase

Ahora, hablemos de clases o interfaces burlonas en Spock.

A veces, nos gustaría saber si algún método del objeto dependiente fue llamado con argumentos específicos . Queremos centrarnos en el comportamiento de los objetos y explorar cómo interactúan observando las llamadas al método.Burlarse es una descripción de la interacción obligatoria entre los objetos en la clase de prueba.

Probaremos las interacciones en el código de ejemplo que describimos a continuación.

5.1. Código con interacción

Para un ejemplo simple, vamos a guardar elementos en la base de datos. Después del éxito, queremos publicar un evento en el corredor de mensajes sobre nuevos elementos en nuestro sistema.

El corredor de mensajes de ejemplo es RabbitMQ o Kafka , por lo que, en general, solo describiremos nuestro contrato:

public interface EventPublisher { void publish(String addedOfferId); }

Nuestro método de prueba guardará los elementos que no estén vacíos en la base de datos y luego publicará el evento. Guardar el elemento en la base de datos es irrelevante en nuestro ejemplo, así que solo pondremos un comentario:

void saveItems(List itemIds) { List notEmptyOfferIds = itemIds.stream() .filter(itemId -> !itemId.isEmpty()) .collect(Collectors.toList()); // save in database notEmptyOfferIds.forEach(eventPublisher::publish); }

5.2. Verificación de la interacción con objetos simulados

Ahora, probemos la interacción en nuestro código.

Primero, necesitamos simular EventPublisher en nuestro método setup () . Entonces, básicamente, creamos un nuevo campo de instancia y lo simulamos usando la función Mock (Clase) :

class ItemServiceTest extends Specification { ItemProvider itemProvider ItemService itemService EventPublisher eventPublisher def setup() { itemProvider = Stub(ItemProvider) eventPublisher = Mock(EventPublisher) itemService = new ItemService(itemProvider, eventPublisher) }

Ahora, podemos escribir nuestro método de prueba. Pasaremos 3 cadenas: ”, 'a', 'b' y esperamos que nuestro eventPublisher publique 2 eventos con cadenas 'a' y 'b':

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') }

Echemos un vistazo más de cerca a nuestra afirmación en la final a continuación, la sección:

1 * eventPublisher.publish('a')

Esperamos que itemService llame a un eventPublisher.publish (String) con 'a' como argumento.

En stubbing, hemos hablado de restricciones de argumentos. Las mismas reglas se aplican a los simulacros. Podemos verificar que eventPublisher.publish (String) fue llamado dos veces con cualquier argumento no nulo y no vacío:

2 * eventPublisher.publish({ it != null && !it.isEmpty() })

5.3. Combinando burla y golpe

En Spock, un Mock puede comportarse igual que un Stub . Entonces, podemos decirle a los objetos simulados que, para una llamada a un método dado, debe devolver los datos dados.

Anulemos un ItemProvider con Mock (Class) y creemos un nuevo ItemService :

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 reescribir el stubbing de la sección dada :

1 * itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]

Por lo general, esta línea dice: itemProvider.getItems será llamado una vez con el argumento ['item-'id'] y devolverá la matriz dada .

We already know that mocks can behave the same as stubs. All of the rules regarding argument constraints, returning multiple values, and side-effects also apply to Mock.

6. Spying Classes in Spock

Spies provide the ability to wrap an existing object. This means we can listen in on the conversation between the caller and the real object but retain the original object behavior. Basically, Spy delegates method calls to the original object.

In contrast to Mock and Stub, we can't create a Spy on an interface. It wraps an actual object, so additionally, we will need to pass arguments for the constructor. Otherwise, the type's default constructor will be invoked.

6.1. Code Under Test

Let's create a simple implementation for EventPublisher. LoggingEventPublisher will print in the console the id of every added item. Here's the interface method implementation:

@Override public void publish(String addedOfferId) { System.out.println("I've published: " + addedOfferId); }

6.2. Testing with Spy

We create spies similarly to mocks and stubs, by using the Spy(Class) method. LoggingEventPublisher does not have any other class dependencies, so we don't have to pass constructor args:

eventPublisher = Spy(LoggingEventPublisher)

Now, let's test our spy. We need a new instance of ItemService with our spied object:

given: eventPublisher = Spy(LoggingEventPublisher) itemService = new ItemService(itemProvider, eventPublisher) when: itemService.saveItems(['item-id']) then: 1 * eventPublisher.publish('item-id')

We verified that the eventPublisher.publish method was called only once. 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

Note that when we use stub on a method of Spy, then it won't call the real object method. Generally, we should avoid using spies. If we have to do it, maybe we should rearrange the code under specification?

7. Good Unit Tests

Let's end with a quick summary of how the use of mocked objects improves our tests:

  • we create deterministic test suites
  • we won't have any side effects
  • our unit tests will be very fast
  • we can focus on the logic contained in a single Java class
  • our tests are independent of the environment

8. Conclusion

En este artículo, describimos a fondo espías, burlas y talones en Groovy . El conocimiento sobre este tema hará que nuestras pruebas sean más rápidas, más confiables y más fáciles de leer.

La implementación de todos nuestros ejemplos se puede encontrar en el proyecto Github.