A diferencia de muchas plataformas, Java adolece de una falta de bibliotecas de stub de conexi贸n. Si has estado en este mundo durante mucho tiempo, entonces probablemente deber铆as estar familiarizado con WireMock, Betamax o incluso Spock. Muchos desarrolladores en las pruebas usan Mockito para describir el comportamiento de los objetos, DataJpaTest con una base de datos h2 local, pruebas de Cucumber. Hoy conocer谩 una alternativa liviana que lo ayudar谩 a lidiar con varios problemas que puede encontrar al usar estos enfoques. En particular, anyStub intenta resolver los siguientes problemas:
- simplificar la configuraci贸n del entorno de prueba
- automatizar la recopilaci贸n de datos para pruebas
- mant茅ngase en la prueba de su aplicaci贸n y evite probar para otra cosa
驴Qu茅 es anyStub y c贸mo funciona?
AnyStub ajusta las llamadas de funci贸n, tratando de encontrar las llamadas coincidentes que ya se han grabado. Pueden suceder dos cosas con esto:
- si hay una llamada coincidente, anyStub restaurar谩 el resultado registrado asociado con esa llamada y lo devolver谩
- si no hay una llamada coincidente y se permite el acceso al sistema externo, anyStub har谩 esta llamada, registrar谩 este resultado y lo devolver谩
Fuera de la caja, anyStub proporciona envoltorios para el cliente http de Apache HttpClient para crear ap茅ndices para solicitudes http y varias interfaces de javax.sql. * Para conexiones de base de datos. Tambi茅n se le proporciona una API para crear ap茅ndices para otras conexiones.
AnyStub es una biblioteca de clases simple y no requiere una configuraci贸n especial de su entorno. Esta biblioteca est谩 dise帽ada para trabajar con aplicaciones de arranque por resorte y obtendr谩 el m谩ximo beneficio al seguir esta ruta. Puede usarlo fuera de Spring, en aplicaciones Java simples, pero definitivamente tendr谩 que hacer un trabajo adicional. La siguiente descripci贸n se centra en probar aplicaciones de arranque por resorte.
Veamos las pruebas de integraci贸n. Esta es la forma m谩s emocionante y completa de probar su sistema. De hecho, spring-boot y JUnit hacen casi todo por ti cuando escribes anotaciones m谩gicas:
@RunWith(SpringRunner.class) @SpringBootTest
En la actualidad, las pruebas de integraci贸n se subestiman y se utilizan de forma limitada, y algunos desarrolladores las evitan. Esto se debe principalmente a la larga preparaci贸n y mantenimiento de las pruebas o la necesidad de una configuraci贸n especial del entorno en los servidores de compilaci贸n.
Con anyStub, no tiene que paralizar el contexto de primavera. En cambio, mantener el contexto cercano a la configuraci贸n de producci贸n es simple y directo.
En este ejemplo, veremos c贸mo conectar anyStub a un Consuming a RESTful Web Service del manual de Pivotal.
Conectar una biblioteca a trav茅s de pom.xml
<dependency> <groupId>org.anystub</groupId> <artifactId>anystub</artifactId> <version>0.2.27</version> <scope>test</scope> </dependency>
El siguiente paso es modificar el contexto de primavera.
package hello; import org.anystub.http.StubHttpClient; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class TestConfiguration { @Bean public RestTemplateBuilder builder() { RestTemplateCustomizer restTemplateCustomizer = new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { HttpClient real = HttpClientBuilder.create().build(); StubHttpClient stubHttpClient = new StubHttpClient(real); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(stubHttpClient); restTemplate.setRequestFactory(requestFactory); } }; return new RestTemplateBuilder(restTemplateCustomizer); } }
Esta modificaci贸n no cambia las relaciones de componentes en la aplicaci贸n, sino que solo reemplaza la implementaci贸n de una 煤nica interfaz. Esto nos env铆a al Principio de sustituci贸n de Barbara Lisk . Si el dise帽o de su aplicaci贸n no lo viola, esta sustituci贸n no violar谩 la funcionalidad.
Todo esta listo. Este proyecto ya incluye una prueba.
@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Autowired private RestTemplate restTemplate; @Test public void contextLoads() { assertThat(restTemplate).isNotNull(); } }
Esta prueba est谩 vac铆a, pero ya est谩 ejecutando el contexto de la aplicaci贸n. La diversi贸n comienza aqu铆 . Como dijimos anteriormente, el contexto de la aplicaci贸n en la prueba coincide con el contexto de trabajo en el que se crea CommandLineRunner en el que se ejecuta la solicitud http al sistema externo.
@SpringBootApplication public class Application { private static final Logger log = LoggerFactory.getLogger(Application.class); public static void main(String args[]) { SpringApplication.run(Application.class); } @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); } @Bean public CommandLineRunner run(RestTemplate restTemplate) throws Exception { return args -> { Quote quote = restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); log.info(quote.toString()); }; } }
Esto es suficiente para demostrar el funcionamiento de la biblioteca. Despu茅s de comenzar las pruebas por primera vez, encontrar谩 el nuevo complete/src/test/resources/anystub/stub.yml
.
request0: exception: [] keys: [GET, HTTP/1.1, 'https://gturnquist-quoters.cfapps.io/api/random'] values: [HTTP/1.1, '200', OK, 'Content-Type: application/json;charset=UTF-8', 'Date: Thu, 25 Apr 2019 23:04:49 GMT', 'X-Vcap-Request-Id: 5ffce9f3-d972-4e95-6b5c-f88f9b0ae29b', 'Content-Length: 177', 'Connection: keep-alive', '{"type":"success","value":{"id":3,"quote":"Spring has come quite a ways in addressing developer enjoyment and ease of use since the last time I built an application using it."}}']
Que paso spring-boot ha incorporado RestTemplateBuilder desde la configuraci贸n de prueba a la aplicaci贸n. Esto llev贸 a la aplicaci贸n a trabajar a trav茅s de la implementaci贸n del c贸digo auxiliar del cliente http. StubHttpClient intercept贸 la solicitud, no encontr贸 el archivo ap茅ndice, ejecut贸 la solicitud, guard贸 el resultado en un archivo y devolvi贸 el resultado recuperado del archivo.
A partir de ahora, puede ejecutar esta prueba sin conexi贸n a Internet y esta solicitud ser谩 exitosa. restTemplate.getForObject()
devolver谩 el mismo resultado. Puede confiar en este hecho en sus futuras pruebas.
Puede encontrar todos los cambios descritos en GitHub .
De hecho, todav铆a no hemos creado una sola prueba. Antes de escribir pruebas, veamos c贸mo funciona con las bases de datos.
En este ejemplo, agregaremos una prueba de integraci贸n para acceder a datos relacionales usando JDBC con Spring del tutorial fundamental.
La configuraci贸n de prueba para este caso se ve as铆:
package hello; import org.anystub.jdbc.StubDataSource; import org.h2.jdbcx.JdbcDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class TestConfiguration { @Bean public DataSource dataSource() { JdbcDataSource ds = new JdbcDataSource(); ds.setURL("jdbc:h2:./test"); return new StubDataSource(ds); } }
Aqu铆, se crea un origen de datos regular a una base de datos externa y se envuelve con una implementaci贸n de c贸digo auxiliar: la clase StubDataSource. Spring-boot lo incorpora en contexto. Tambi茅n necesitamos crear al menos una prueba para ejecutar el contexto de primavera en la prueba.
package hello; import org.anystub.AnyStubId; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Test @AnyStubId public void test() { } }
Esto es nuevamente una prueba vac铆a: su 煤nica tarea es ejecutar el contexto de la aplicaci贸n. Aqu铆 vemos una anotaci贸n muy importante @AnystubId
, pero a煤n no estar谩 involucrada.
Despu茅s de la primera ejecuci贸n, encontrar谩 un nuevo src/test/resources/anystub/stub.yml
que incluye todas las llamadas a la base de datos. Te sorprender谩 c贸mo funciona la primavera detr谩s de escena con bases de datos. Tenga en cuenta que las nuevas ejecuciones de la prueba no conducir谩n a un acceso real a la base de datos. Si elimina test.mv.db, no aparecer谩 despu茅s de repetidas ejecuciones de las pruebas. El conjunto completo de cambios se puede ver en GitHub .
Para resumir. con anyStub:
- no necesita configurar espec铆ficamente un entorno de prueba
- las pruebas se realizan con datos reales
- la primera ejecuci贸n de las pruebas prueba sus suposiciones y guarda los datos de la prueba, las siguientes verifican que el sistema no se haya degradado
Probablemente tenga preguntas: 驴c贸mo cubre esto los casos en que la base de datos a煤n no existe, qu茅 hacer con las pruebas negativas y el manejo de excepciones? Volveremos a esto, pero primero, nos ocuparemos de escribir pruebas simples.
Ahora estamos experimentando con Consumir un servicio web RESTful . Este proyecto no contiene componentes que se puedan probar. A continuaci贸n se crean dos clases, que deben representar dos capas de un dise帽o de arquitectura.
package hello; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class DataProvider { private final RestTemplate restTemplate; public DataProvider(RestTemplate restTemplate) { this.restTemplate = restTemplate; } Quote provideData() { return restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); } }
DataProvider proporciona acceso a datos en vol谩til sistema externo
package hello; import org.springframework.stereotype.Component; @Component public class DataProcessor { private final DataProvider dataProvider; public DataProcessor(DataProvider dataProvider) { this.dataProvider = dataProvider; } int processData() { return dataProvider.provideData().getValue().getQuote().length(); } }
DataProcessor procesar谩 datos de un sistema externo.
Tenemos la intenci贸n de probar el DataProcessor
. Es necesario probar la correcci贸n del algoritmo de procesamiento y proteger el sistema de la degradaci贸n de futuros cambios.
Para lograr estos objetivos, puede considerar crear un objeto simulado DataProvider con un conjunto de datos y pasarlo al constructor DataProcessor en las pruebas. Otra forma podr铆a ser descomponer el DataProcessor para resaltar el procesamiento de la clase Quote. Entonces, tal clase es f谩cil de probar usando pruebas unitarias (seguramente, este es el m茅todo recomendado en libros respetados sobre c贸digo limpio). Intentemos evitar los cambios de c贸digo y la invenci贸n de los datos de prueba y simplemente escriba una prueba.
@RunWith(SpringRunner.class) @SpringBootTest public class DataProcessorTest { @Autowired private DataProcessor dataProcessor; @Test @AnyStubId(filename = "stub") public void processDataTest() { assertEquals(131, dataProcessor.processData()); } }
Es hora de hablar sobre la anotaci贸n @AnystubId. Esta anotaci贸n ayuda a administrar y controlar archivos de c贸digo auxiliar en las pruebas. Se puede usar con una clase de prueba o su m茅todo. Esta anotaci贸n configura un archivo ap茅ndice individual para el 谩rea correspondiente. Si cualquier 谩rea est谩 cubierta simult谩neamente por anotaciones de nivel de clase y m茅todo, la anotaci贸n de m茅todo tiene prioridad. Esta anotaci贸n tiene el par谩metro de nombre de archivo, que define el nombre del archivo ap茅ndice. la extensi贸n ".yml" se agrega autom谩ticamente si se omite. Al ejecutar esta prueba, no encontrar谩 un nuevo archivo. El src/test/resources/anystub/stub.yml
ya se cre贸 anteriormente y esta prueba lo reutilizar谩. Obtuvimos el n煤mero 131 de este c贸digo analizando el resultado de la consulta.
@Test @AnyStubId public void processDataTest2() { assertEquals(131, dataProcessor.processData()); Base base = getStub(); assertEquals(1, base.times("GET")); assertTrue(base.history().findFirst().get().matchEx_to(null, null, ".*gturnquist-quoters.cfapps.io.*")); }
En esta prueba, la anotaci贸n @AnyStubId aparece sin el par谩metro de nombre de archivo. En este caso, src/test/resources/anystubprocessDataTest2.yml
. El nombre del archivo se crea a partir del nombre de la funci贸n (clase) + ".yml". Una vez que anyStub crea un nuevo archivo para esta prueba, debe realizar una llamada real al sistema. Y es nuestra suerte que la nueva cita tenga la misma longitud. Las dos 煤ltimas comprobaciones muestran c贸mo probar el comportamiento de la aplicaci贸n. Est谩 disponible para usted: seleccionando consultas por par谩metros o partes de par谩metros y contando el n煤mero de consultas. Hay varias variaciones de los tiempos y las funciones de coincidencia que se pueden encontrar en la documentaci贸n .
@Test @AnyStubId(requestMode = RequestMode.rmTrack) public void processDataTest3() { assertEquals(79, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); assertEquals(168, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); Base base = getStub(); assertEquals(4, base.times("GET")); }
En esta prueba, @AnyStubId aparece con el nuevo par谩metro requestMode. Le permite administrar permisos para archivos stub. Hay dos aspectos a controlar: b煤squeda de archivos y permiso para llamar a un sistema externo.
RequestMode.rmTrack
establece las siguientes reglas: si el archivo acaba de crearse, todas las solicitudes se env铆an al sistema externo y se escriben en el archivo con las respuestas, independientemente de si hay una solicitud id茅ntica en el archivo (se permiten duplicados en el archivo). Si antes de ejecutar las pruebas existe el archivo ap茅ndice, las solicitudes al sistema externo est谩n prohibidas. Las llamadas se esperan exactamente en la misma secuencia. Si la siguiente solicitud no coincide con la solicitud en el archivo, se genera una excepci贸n.
RequestMode.rmNew
este modo est谩 activado por defecto. Cada solicitud se busca en el archivo ap茅ndice. Si se encuentra una solicitud coincidente: el resultado correspondiente se restaura desde el archivo, la solicitud al sistema externo se pospone. Si no se encuentra la solicitud, se solicita el sistema externo, el resultado se guarda en un archivo. Solicitudes duplicadas en el archivo: no se producen.
RequestMode.rmNone
Cada solicitud se busca en un archivo RequestMode.rmNone
. Si se encuentra una consulta coincidente, su resultado se restaura desde el archivo. Si la prueba genera una solicitud que no est谩 en el archivo, se genera una excepci贸n.
RequestMode.rmAll
antes de la primera solicitud, se borra el archivo RequestMode.rmAll
. Todas las solicitudes se escriben en el archivo (se permiten duplicados en el archivo). Puede usar este modo si desea ver c贸mo funciona la conexi贸n.
RequestMode.rmPassThrough
todas las solicitudes se env铆an directamente al sistema externo, sin pasar por el c贸digo auxiliar de implementaci贸n.
Estos cambios est谩n disponibles en GitHub.
Que mas
Vimos c贸mo anyStub guarda las respuestas. Si se produce una excepci贸n al acceder a un sistema externo, anyStub lo guardar谩 y lo reproducir谩 en solicitudes posteriores.
A menudo, las clases de nivel superior generan excepciones, mientras que las clases de conexi贸n reciben una respuesta v谩lida (probablemente con un c贸digo de error). En este caso, anyStub es responsable de reproducir la respuesta con el c贸digo de error, y las clases de nivel superior tambi茅n arrojar谩n excepciones para sus pruebas.
Agregue archivos de resguardo al repositorio.
No tenga miedo de eliminar y sobrescribir archivos de c贸digo auxiliar.
Administre archivos stub con prudencia. Puede reutilizar un archivo en varias pruebas o proporcionar un archivo individual para cada prueba. Aproveche esta oportunidad para sus necesidades. Pero, por lo general, usar un solo archivo con diferentes modos de acceso es una mala idea.
Estas son todas las caracter铆sticas principales de anyStub.