¡Responderás por todo! Contratos dirigidos por el consumidor a través de los ojos del desarrollador

En este artículo, hablaremos sobre los problemas que resuelve Consumer Driven Contracts y mostraremos cómo aplicarlo utilizando el ejemplo de Pact with Node.js y Spring Boot. Y habla sobre las limitaciones de este enfoque.


Problema


Cuando se prueban productos, a menudo se usan pruebas de escenarios en las que se verifica la integración de varios componentes del sistema en un entorno especialmente seleccionado. Tales pruebas en los servicios en vivo dan el resultado más confiable (sin contar las pruebas en la batalla). Pero al mismo tiempo, son uno de los más caros.

  • A menudo se cree erróneamente que el entorno de integración no debe ser tolerante a fallas. SLA, las garantías para tales entornos rara vez se expresan, pero si no está disponible, los equipos tienen que retrasar los lanzamientos o esperar lo mejor e ir a la batalla sin pruebas. Aunque todos saben que la esperanza no es una estrategia . Y las nuevas tecnologías de infraestructura solo complican el trabajo con entornos de integración.
  • Otro dolor es trabajar con datos de prueba . Muchos escenarios requieren un cierto estado del sistema, accesorios. ¿Qué tan cerca deberían estar de combatir los datos? ¿Cómo actualizarlos antes de la prueba y limpiarlos una vez finalizados?
  • Las pruebas son demasiado inestables . Y no solo por la infraestructura que mencionamos en el primer párrafo. ¡La prueba puede fallar porque un equipo vecino lanzó sus propios controles que rompieron el estado esperado del sistema! Muchos cheques falsos negativos, pruebas escamosas terminan sus vidas en @Ignored . Además, diferentes partes de la integración pueden ser compatibles con diferentes equipos. Lanzaron un nuevo candidato de lanzamiento con errores: rompieron a todos los consumidores. Alguien resuelve este problema con bucles de prueba dedicados. Pero a costa de multiplicar el costo del soporte.
  • Tales pruebas toman mucho tiempo . Incluso teniendo en cuenta la automatización, se pueden esperar resultados durante horas.
  • Y para colmo, si la prueba realmente cayó bien, está lejos de ser siempre posible encontrar inmediatamente la causa del problema. Puede esconderse profundamente detrás de las capas de integración. O puede ser el resultado de una combinación inesperada de estados de muchos componentes del sistema.

Las pruebas estables en un entorno de integración requieren una inversión seria de control de calidad, desarrollo e incluso operaciones. No es de extrañar que estén en la cima de la pirámide de prueba . Tales pruebas son útiles, pero la economía de recursos no les permite verificarlo todo. La principal fuente de su valor es el medio ambiente.

Debajo de la misma pirámide hay otras pruebas en las que intercambiamos confianza por pequeños dolores de cabeza de soporte, utilizando comprobaciones de aislamiento. Cuanto más granular, cuanto menor sea la escala de la prueba, menor será la dependencia del entorno externo. En la parte inferior de la pirámide hay pruebas unitarias. Verificamos funciones individuales, clases, operamos no tanto con la semántica empresarial como con las construcciones de una implementación específica. Estas pruebas dan retroalimentación rápida.

Pero tan pronto como bajemos por la pirámide, tenemos que reemplazar el medio ambiente con algo. Los talones aparecen, como servicios completos y entidades individuales del lenguaje de programación. Es con la ayuda de enchufes que podemos probar los componentes de forma aislada. Pero también reducen la validez de los cheques. ¿Cómo asegurarse de que el código auxiliar devuelve los datos correctos? ¿Cómo garantizar su calidad?

La solución puede ser una documentación completa que describa varios escenarios y posibles estados de los componentes del sistema. Pero cualquier formulación aún deja libertad de interpretación. Por lo tanto, una buena documentación es un artefacto vivo que mejora constantemente a medida que el equipo comprende el área del problema. ¿Cómo, entonces, garantizar el cumplimiento de los talones de documentación?

En muchos proyectos, puede observar una situación en la que los talones están escritos por los mismos tipos que desarrollaron el artefacto de prueba. Por ejemplo, los desarrolladores de aplicaciones móviles hacen stubs para sus propias pruebas. Como resultado, los programadores pueden comprender la documentación a su manera (lo cual es completamente normal), crean el código auxiliar con el comportamiento esperado incorrecto, escriben el código de acuerdo con él (con pruebas verdes) y se generan errores de integración reales.

Además, la documentación generalmente se mueve hacia abajo: los clientes usan especificaciones de servicios (en este caso, otro servicio puede ser un cliente del servicio). No expresa cómo los consumidores usan los datos, qué datos se necesitan en absoluto, qué suposiciones hacen para esos datos. La consecuencia de esta ignorancia es la ley de Hyrum .



Hyrum Wright ha estado desarrollando herramientas públicas dentro de Google durante mucho tiempo y ha observado cómo los cambios más pequeños pueden causar fallas en los clientes que utilizaron las funciones implícitas (indocumentadas) de sus bibliotecas. Dicha conectividad oculta complica la evolución de la API.

Estos problemas pueden resolverse hasta cierto punto mediante contratos dirigidos por el consumidor. Al igual que cualquier enfoque y herramienta, tiene un rango de aplicabilidad y costo, que también consideraremos. Las implementaciones de este enfoque han alcanzado un nivel de madurez suficiente para probar sus proyectos.

¿Qué es un CDC?


Tres elementos clave:

  • El contrato Descrito usando algunos DSL, dependiendo de la implementación. Contiene una descripción de la API en forma de escenarios de interacción: si llega una solicitud específica, entonces el cliente debe recibir una respuesta específica.
  • Pruebas de clientes . Además, utilizan un trozo, que se genera automáticamente a partir del contrato.
  • Pruebas para la API . También se generan a partir del contrato.

Por lo tanto, el contrato es ejecutable. Y la característica principal del enfoque es que los requisitos para el comportamiento de la API van desde el cliente hasta el servidor.

El contrato se centra en el comportamiento que realmente le importa al consumidor. Hace explícitas sus suposiciones sobre la API.

El objetivo principal de los CDC es brindar una comprensión del comportamiento de la API a sus desarrolladores y a los desarrolladores de sus clientes. Este enfoque se combina bien con BDD, en las reuniones de tres amigos puede esbozar los espacios en blanco para el contrato. En definitiva, este contrato también sirve para mejorar las comunicaciones; compartir una comprensión común del área del problema e implementar la solución dentro y entre los equipos.

Pacto


Considere usar CDC como ejemplo de Pact, una de sus implementaciones. Supongamos que hacemos una aplicación web para los participantes de la conferencia. En la próxima iteración, el equipo desarrolla un cronograma de presentación, hasta ahora sin historias como votación o notas, solo la salida de la cuadrícula de informes. El código fuente para el ejemplo está aquí .

En una reunión de tres cuatro amigos, se reúnen un producto, un probador, desarrolladores del backend y una aplicación móvil. Dicen que

  • Se mostrará una lista con el texto en la interfaz de usuario: Título del informe + Ponentes + Fecha y hora.
  • Para hacer esto, el back-end debe devolver datos como en el ejemplo a continuación.

 { "talks":[ { "title":"      ", "speakers":[ { "name":" " } ], "time":"2019-05-27T12:00:00+03:00" } ] } 

Después de lo cual el desarrollador frontend va a escribir el código del cliente (backend para frontend). Instala una biblioteca de contrato de pacto en el proyecto:

 yarn add --dev @pact-foundation/pact 

Y comienza a escribir una prueba. Configura el servidor de código auxiliar local, que simulará el servicio con programaciones de informes:

 const provider = new Pact({ //      consumer: "schedule-consumer", provider: "schedule-producer", // ,     port: pactServerPort, //  pact     log: path.resolve(process.cwd(), "logs", "pact.log"), // ,     dir: path.resolve(process.cwd(), "pacts"), logLevel: "WARN", //  DSL  spec: 2 }); 

El contrato es un archivo JSON que describe los escenarios en los que el cliente interactúa con el servicio. Pero no es necesario que lo describa manualmente, ya que se forma a partir de la configuración del código auxiliar en el código. El desarrollador antes de la prueba describe el siguiente comportamiento.

 provider.setup().then(() => provider .addInteraction({ uponReceiving: "a request for schedule", withRequest: { method: "GET", path: "/schedule" }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json;charset=UTF-8" }, body: { talks: [ { title: "      ", speakers: [ { name: " " } ], time: "2019-05-27T12:00:00+03:00" } ] } } }) .then(() => done()) ); 

Aquí, en el ejemplo, especificamos la solicitud de servicio esperada específica, pero pact-js también admite varios métodos para determinar coincidencias .

Finalmente, el programador escribe una prueba de esa parte del código que usa este código auxiliar. En el siguiente ejemplo, lo llamaremos directamente por simplicidad.

 it("fetches schedule", done => { fetch(`http://localhost:${pactServerPort}/schedule`) .then(response => response.json()) .then(json => expect(json).toStrictEqual({ talks: [ { title: "      ", speakers: [ { name: " " } ], time: "2019-05-27T12:00:00+03:00" } ] })) .then(() => done()); }); 

En un proyecto real, esto puede ser una prueba de unidad rápida de una función de interpretación de respuesta separada o una prueba de IU lenta para mostrar los datos recibidos de un servicio.

Durante la ejecución de la prueba, pact verifica que el código auxiliar recibió la solicitud especificada en las pruebas. Las discrepancias pueden verse como diferencias en el archivo pact.log.

 E, [2019-05-21T01:01:55.810194 #78394] ERROR -- : Diff with interaction: "a request for schedule" Diff -------------------------------------- Key: - is expected + is actual Matching keys and values are not shown { "headers": { - "Accept": "application/json" + "Accept": "*/*" } } Description of differences -------------------------------------- * Expected "application/json" but got "*/*" at $.headers.Accept 


Si la prueba tiene éxito, se genera un contrato en formato JSON. Describe el comportamiento esperado de la API.

 { "consumer": { "name": "schedule-consumer" }, "provider": { "name": "schedule-producer" }, "interactions": [ { "description": "a request for schedule", "request": { "method": "GET", "path": "/schedule", "headers": { "Accept": "application/json" } }, "response": { "status": 200, "headers": { "Content-Type": "application/json;charset=UTF-8" }, "body": { "talks":[ { "title":"      ", "speakers":[ { "name":" " } ], "time":"2019-05-27T12:00:00+03:00" } ] }}} ], "metadata": { "pactSpecification": { "version": "2.0.0" } } } 

Él le da este contrato al desarrollador de back-end. Digamos que la API está en Spring Boot. Pact tiene una biblioteca pact -jvm-provider-spring que puede funcionar con MockMVC. Pero echaremos un vistazo al Spring Cloud Contract, que implementa los CDC en el ecosistema de Spring. Utiliza su propio formato de contratos, pero también tiene un punto de extensión para conectar convertidores de otros formatos. Su formato de contrato nativo solo es compatible con el propio Spring Cloud Contract, a diferencia de Pact, que tiene bibliotecas para JVM, Ruby, JS, Go, Python, etc.

Supongamos, en nuestro ejemplo, que el desarrollador de back-end usa Gradle para construir el servicio. Conecta las siguientes dependencias:

 buildscript { // ... dependencies { classpath "org.springframework.cloud:spring-cloud-contract-pact:2.1.1.RELEASE" } } plugins { id "org.springframework.cloud.contract" version "2.1.1.RELEASE" // ... } // ... dependencies { // ... testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier' } 

Y coloca el contrato del Pacto recibido del frotender en el directorio src/test/resources/contracts .

De él, por defecto, el complemento spring-cloud-contract resta contratos. Durante el ensamblaje, se ejecuta la tarea gradle generateContractTests, que genera la siguiente prueba en el directorio build / generate-test-sources.

 public class ContractVerifierTest extends ContractsBaseTest { @Test public void validate_aggregator_client_aggregator_service() throws Exception { // given: MockMvcRequestSpecification request = given() .header("Accept", "application/json"); // when: ResponseOptions response = given().spec(request) .get("/scheduler"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).isEqualTo("application/json;charset=UTF-8"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).array("['talks']").array("['speakers']").contains("['name']").isEqualTo( /*...*/ ); assertThatJson(parsedJson).array("['talks']").contains("['time']").isEqualTo( /*...*/ ); assertThatJson(parsedJson).array("['talks']").contains("['title']").isEqualTo( /*...*/ ); } } 


Al comenzar esta prueba, veremos un error:

 java.lang.IllegalStateException: You haven't configured a MockMVC instance. You can do this statically 

Dado que podemos usar diferentes herramientas para las pruebas, necesitamos decirle al complemento cuál hemos configurado. Esto se realiza a través de la clase base, que heredará las pruebas generadas a partir de los contratos.

 public abstract class ContractsBaseTest { private ScheduleController scheduleController = new ScheduleController(); @Before public void setup() { RestAssuredMockMvc.standaloneSetup(scheduleController); } } 


Para usar esta clase base durante la generación, debe configurar el complemento gradle spring-cloud-contract.

 contracts { baseClassForTests = 'ru.example.schedule.ContractsBaseTest' } 


Ahora tenemos la siguiente prueba generada:
 public class ContractVerifierTest extends ContractsBaseTest { @Test public void validate_aggregator_client_aggregator_service() throws Exception { // ... } } 

La prueba comienza con éxito, pero falla con un error de verificación: el desarrollador aún no ha escrito la implementación del servicio. Pero ahora puede hacerlo en base a un contrato. Puede asegurarse de poder procesar la solicitud del cliente y devolver la respuesta esperada.

El desarrollador del servicio sabe a través del contrato qué necesita hacer, qué comportamiento implementar.

El pacto puede integrarse más profundamente en el proceso de desarrollo. Puede implementar un agente de Pact que agregue dichos contratos, admita su control de versiones y pueda mostrar un gráfico de dependencia.



La carga de un nuevo contrato generado al intermediario se puede realizar en el paso CI al construir el cliente. Y en el código del servidor indique la carga dinámica del contrato por URL. Spring Cloud Contract también es compatible con esto.

Aplicabilidad de los CDC


¿Cuáles son las limitaciones de los contratos dirigidos por el consumidor?

Para utilizar este enfoque , debe pagar con herramientas adicionales como pacto. Los contratos per se son un artefacto adicional, otra abstracción que debe mantenerse cuidadosamente y aplicarse conscientemente prácticas de ingeniería.

No reemplazan las pruebas e2e , ya que los stubs siguen siendo stubs: modelos de componentes del sistema real, que pueden ser un poco, pero no se corresponden con la realidad. A través de ellos, los escenarios complejos no se pueden verificar.

Además, los CDC no reemplazan las pruebas funcionales de API . Son más caros de soportar que las pruebas de unidades antiguas simples. Los desarrolladores de pactos recomiendan utilizar las siguientes heurísticas: si elimina el contrato y esto no causa errores o malas interpretaciones por parte del cliente, entonces no es necesario. Por ejemplo, no es necesario describir absolutamente todos los códigos de error de API a través de un contrato si el cliente los procesa de la misma manera. En otras palabras, el contrato describe para el servicio solo lo que es importante para su cliente . No más, pero no menos.

Demasiados contratos también complican la evolución de la API. Cada contrato adicional es una ocasión para pruebas rojas . Es necesario diseñar un CDC de tal manera que cada prueba de falla lleve una carga semántica útil que supere el costo de su soporte. Por ejemplo, si el contrato fija la longitud mínima de un determinado campo de texto que es indiferente al consumidor (él usa la técnica Toleran Reader ), entonces cada cambio a este valor mínimo romperá el contrato y los nervios de quienes lo rodean. Dicha verificación debe transferirse al nivel de la API misma e implementarse según la fuente de restricciones.

Conclusión


Los CDC mejoran la calidad del producto al describir explícitamente el comportamiento de integración. Ayuda a los clientes y desarrolladores de servicios a alcanzar un entendimiento común, le permite hablar a través del código. Pero esto tiene el costo de agregar herramientas, introducir nuevas abstracciones y acciones adicionales de los miembros del equipo.

Al mismo tiempo, las herramientas y los marcos de trabajo de los CDC se están desarrollando activamente y ya han alcanzado la madurez para las pruebas en sus proyectos. Prueba :)

En la conferencia QualityConf del 27 al 28 de mayo, Andrei Markelov hablará sobre las técnicas de prueba en el producto, y Arthur Khineltsev hablará sobre el monitoreo de una interfaz muy cargada, cuando el precio de incluso un pequeño error es de decenas de miles de usuarios tristes.

¡Ven a chatear por calidad!

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


All Articles