Cómo construir una pirámide en el tronco o aplicaciones de desarrollo guiado por pruebas en Spring Boot

Spring Framework a menudo se cita como un ejemplo del marco Cloud Native , diseñado para trabajar en la nube, desarrollar aplicaciones de doce factores , microservicios y uno de los productos más estables, pero al mismo tiempo innovadores. Pero en este artículo me gustaría detenerme en un lado más fuerte de Spring: es su soporte de desarrollo a través de pruebas (¿capacidad TDD?). A pesar de la conectividad TDD, a menudo noté que los proyectos de Spring ignoran algunas de las mejores prácticas para las pruebas, inventan sus propias bicicletas o no escriben pruebas porque son "lentas" o "poco confiables". Y le diré exactamente cómo escribir pruebas rápidas y confiables para aplicaciones en Spring Framework y llevar a cabo el desarrollo a través de las pruebas. Entonces, si usa Spring (o quiere comenzar), comprenda qué pruebas son en general (o quiere entender), o piense que contextLoads es el nivel necesario y suficiente de pruebas de integración, ¡será interesante!


La capacidad "TDD" es muy ambigua y poco medible, pero, sin embargo, Spring tiene muchas cosas que, por diseño, ayudan a escribir la integración y las pruebas unitarias con un mínimo esfuerzo. Por ejemplo:


  • Pruebas de integración: puede iniciar fácilmente la aplicación, bloquear componentes, redefinir parámetros, etc.
  • Pruebas de integración de enfoque: solo acceso a datos, solo web, etc.
  • Soporte listo para usar: bases de datos en memoria, colas de mensajes, autenticación y autorización en pruebas
  • Prueba a través de contratos (Spring Cloud Contract)
  • Soporte de pruebas de interfaz de usuario web utilizando HtmlUnit
  • Flexibilidad de la configuración de la aplicación: perfiles, configuraciones de prueba, componentes, etc.
  • Y mucho mas

Para empezar, una pequeña pero necesaria introducción sobre TDD y las pruebas en general.


Desarrollo dirigido por pruebas


TDD se basa en una idea muy simple: escribimos pruebas antes de escribir código. En teoría, suena aterrador, pero después de un tiempo llega una comprensión de las prácticas y técnicas, y la opción de escribir pruebas después causa una incomodidad tangible. Una de las prácticas clave es la iteración , es decir haga todo lo pequeño, las iteraciones enfocadas, cada una de las cuales se describe como un Refactor Rojo-Verde .


En la fase roja , escribimos una prueba de caída, y es muy importante que caiga con una razón y descripción claras y comprensibles, y que la prueba en sí misma esté completa y pase cuando se escriba el código. La prueba debe verificar el comportamiento , no la implementación , es decir siga el enfoque de la caja negra, luego explicaré por qué.


En la fase verde , escribimos el código mínimo necesario para pasar la prueba. A veces es interesante practicar y hacerlo lo más loco posible (aunque es mejor no dejarse llevar) y cuando una función devuelve un valor booleano dependiendo del estado del sistema, el primer "pase" puede simplemente ser return true .


En la fase de refactorización , que solo puede iniciarse cuando todas las pruebas son verdes , refactorizaremos el código y lo pondremos en condiciones adecuadas. Ni siquiera es necesario para un código que escribimos, por lo tanto, es importante comenzar a refactorizar en un sistema estable. El enfoque de "caja negra" solo ayudará a refactorizar, cambiar la implementación, pero no tocar el comportamiento.


Hablaré sobre diferentes aspectos de TDD en el futuro, después de todo, esta es la idea de una serie de artículos, por lo que ahora no me detendré particularmente en los detalles. Pero antes de responder a las críticas estándar de TDD, mencionaré un par de mitos que escucho con frecuencia.


  • "TDD tiene aproximadamente el 100% de cobertura del código, pero no ofrece garantías" : el desarrollo mediante pruebas no tiene ninguna relación con el 100% de cobertura. En muchos equipos donde trabajé, esta métrica ni siquiera se midió, y se clasificó como métrica de vanidad. Y sí, el 100% de cobertura de prueba no significa nada.
  • "TDD funciona solo para funciones simples, una aplicación real con una base de datos y un estado difícil no se puede hacer con ella" es una excusa muy popular, generalmente complementada por "Tenemos una aplicación tan complicada que no escribimos pruebas en absoluto, es imposible". Vi un enfoque TDD funcional en aplicaciones completamente diferentes: web (con y sin SPA), móvil, API, microservicios, monolitos, sistemas bancarios complejos, plataformas en la nube, marcos, plataformas minoristas escritas en diferentes idiomas y tecnologías. Por lo tanto, el mito popular "Somos únicos, todo es diferente" suele ser una excusa para no invertir esfuerzo y dinero en las pruebas, pero no es una razón real (aunque también puede haber razones reales).
  • "Todavía habrá errores con TDD" , por supuesto, como en cualquier otro software. TDD no se trata de errores o su ausencia en absoluto, es una herramienta de desarrollo. Como depuración. Como un IDE Me gusta la documentación. Ninguna de estas herramientas garantiza la ausencia de errores, solo ayudan a hacer frente a la creciente complejidad del sistema.

El objetivo principal de TDD y las pruebas en general es dar al equipo la confianza de que el sistema funciona de manera estable. Por lo tanto, ninguna de las prácticas de prueba determina cuántas y qué pruebas escribir. Escriba cuánto cree que es necesario, cuánto necesita para estar seguro de que en este momento el código se puede poner en producción y funcionará . Hay personas que consideran que las pruebas de integración rápida como una caja negra definitiva son necesarias y suficientes, y las pruebas unitarias son opcionales. Alguien dice que las pruebas de e2e con la posibilidad de un retroceso rápido a la versión anterior y la presencia de lanzamientos canarios no son tan críticos. Cuántos equipos, tantos enfoques, es importante encontrar uno propio.

Uno de mis objetivos es alejarme del formato "desarrollo mediante la prueba de una función que agrega dos números" en la historia de TDD y mirar una aplicación real, un tipo de práctica de prueba que se ha evaporado a una aplicación mínima, recopilada en proyectos reales. Como ejemplo semi-real, usaré una pequeña aplicación web que yo mismo inventé para abstraer fábricas Bakery - Fábrica de pasteles . Planeo escribir pequeños artículos, enfocándome cada vez en una parte separada de la funcionalidad de la aplicación y mostrarle a través de TDD que puede diseñar API, la estructura interna de la aplicación y mantener una refactorización constante.


Un plan de muestra para una serie de artículos, tal como lo veo en este momento, es:


  1. Walking skeleton: marco de aplicación donde puede ejecutar el ciclo Red-Green-Refactor
  2. Pruebas de IU y diseño orientado al comportamiento
  3. Pruebas de acceso a datos (Spring Data)
  4. Autorización y pruebas de autenticación (Spring Security)
  5. Jet Stack (Reactor de proyecto WebFlux +)
  6. Interoperabilidad de (micro) servicios y contratos (Spring Cloud)
  7. Prueba de Message Queue Server (Spring Cloud)

Este artículo introductorio tratará sobre los puntos 1 y 2: crearé un marco de aplicación y una prueba de interfaz de usuario básica usando el BDD, o enfoque de desarrollo basado en el comportamiento . Cada artículo comenzará con una historia de usuario , pero no hablaré sobre la parte del "producto" para ahorrar tiempo. La historia del usuario se escribirá en inglés, pronto quedará claro por qué. Todos los ejemplos de código se pueden encontrar en GitHub, por lo que no analizaré todo el código, solo las partes importantes.


La historia de usuario es una descripción de una característica de una aplicación de lenguaje natural que generalmente se escribe en nombre de un usuario del sistema.

Historia de usuario 1: el usuario ve la página de bienvenida


Como Alice, un nuevo usuario
Quiero ver una página de bienvenida cuando visite el sitio web de Cake Factory
Para saber cuándo Cake Factory está a punto de lanzarse

Criterios de aceptación:
Escenario: un usuario que visita la visita al sitio web antes de la fecha de lanzamiento
Dado que soy un nuevo usuario
Cuando visito el sitio web de Cake Factory
Luego veo un mensaje 'Gracias por su interés'
Y veo un mensaje 'El sitio web llegará pronto ...'

Tomará conocimiento: lo que es el desarrollo impulsado por el comportamiento y el pepino , los fundamentos de las pruebas de arranque de primavera .


La primera historia de usuario es bastante básica, pero el objetivo aún no está en la complejidad, sino en la creación del esqueleto para caminar , una aplicación mínima para comenzar el ciclo TDD .


Después de crear un nuevo proyecto en Spring Initializr con módulos Web y Moustache, para empezar necesitaré algunos cambios más para build.gradle :


  • agregue HtmlUnit testImplementation('net.sourceforge.htmlunit:htmlunit') . No necesita especificar la versión, el complemento de administración de dependencias Spring Boot para Gradle seleccionará automáticamente la versión necesaria y compatible
  • migrar un proyecto de JUnit 4 a JUnit 5 (porque 2018 está en el patio)
  • agregar dependencias a Cucumber, una biblioteca que usaré para escribir especificaciones de BDD
  • eliminar creado por defecto CakeFactoryApplicationTests con contextLoads inevitables

En general, este es el "esqueleto" básico de la aplicación, ya puede escribir la primera prueba.


Para facilitar la navegación en el código, hablaré brevemente sobre las tecnologías utilizadas.


Pepino


Cucumber es un marco de desarrollo basado en el comportamiento que ayuda a crear "especificaciones ejecutables", es decir ejecutar pruebas (especificaciones) escritas en lenguaje natural. El complemento Cucumber analiza el código fuente en Java (y muchos otros idiomas) y utiliza definiciones de pasos para ejecutar código real. Las definiciones de paso son métodos de clase anotados por @Given , @When , @Then y otras anotaciones.


Unidad HTML


La página de inicio del proyecto llama a HtmlUnit "un navegador sin GUI para aplicaciones Java". A diferencia de Selenium, HtmlUnit no inicia un navegador real y, lo más importante, no representa la página en absoluto, trabajando directamente con el DOM. JavaScript es compatible a través del motor Mozilla Rhino. HtmlUnit es muy adecuado para aplicaciones clásicas, pero no es muy amigable con las aplicaciones de una sola página. Para empezar, será suficiente, y luego intentaré mostrar que incluso cosas como un marco de prueba pueden formar parte de la implementación, y no la base de la aplicación.


Primera prueba


Ahora una historia de usuario escrita en inglés me será útil. El mejor desencadenante para comenzar la próxima iteración de TDD son los criterios de aceptación escritos de tal manera que puedan convertirse en una especificación ejecutable con un mínimo de gestos.


Idealmente, las historias de los usuarios deben escribirse de modo que simplemente puedan copiarse a la especificación BDD y ejecutarse. Esto está lejos de ser siempre simple y no siempre posible, pero este debería ser el objetivo del propietario del producto y de todo el equipo, aunque no siempre es posible.

Entonces, mi primera característica.


 Feature: Welcome page Scenario: a user visiting the web-site visit before the launch date Given a new user, Alice When she visits Cake Factory web-site Then she sees a message 'Thank you for your interest' And she sees a message 'The web-site is coming in December!' 

Si genera descripciones de pasos (el complemento Intellij IDEA ayuda a Gherkin a ayudar mucho) y ejecuta la prueba, entonces, por supuesto, será verde, todavía no prueba nada. Y aquí viene la fase importante de trabajar en la prueba: debe escribir una prueba, como si el código principal estuviera escrito .


A menudo, para aquellos que comienzan a desterrar la TDD, se produce un estupor aquí: es difícil poner en la cabeza los algoritmos y la lógica de algo que aún no existe. Y por lo tanto, es muy importante tener iteraciones tan pequeñas y enfocadas como sea posible, comenzando desde la historia del usuario y bajando al nivel de integración y unidad. Es importante centrarse en una prueba a la vez e intentar mojarse e ignorar las dependencias que aún no son importantes. A veces me di cuenta de cómo las personas se dejan de lado fácilmente: crean una interfaz o clase para una dependencia, generan inmediatamente una clase de prueba vacía, se agrega una dependencia más allí, se crea otra interfaz y así sucesivamente.


Si la historia es "sería necesario actualizar el estado al guardar", es muy difícil automatizar y formalizar. En mi ejemplo, cada paso se puede establecer claramente en una secuencia de pasos que se pueden describir por código. Está claro que este es el ejemplo más simple y no muestra mucho, pero espero que, con una complejidad cada vez mayor, sea más interesante.

Rojo


Entonces, para mi primera función, creé las siguientes descripciones de pasos:


 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WelcomePage { private WebClient webClient; private HtmlPage page; @LocalServerPort private int port; private String baseUrl; @Before public void setUp() { webClient = new WebClient(); baseUrl = "http://localhost:" + port; } @Given("a new user, Alice") public void aNewUser() { // nothing here, every user is new by default } @When("she visits Cake Factory web-site") public void sheVisitsCakeFactoryWebSite() throws IOException { page = webClient.getPage(baseUrl); } @Then("she sees a message {string}") public void sheSeesAMessageThanksForYourInterest(String expectedMessage) { assertThat(page.getBody().asText()).contains(expectedMessage); } } 

Un par de puntos a los que prestar atención:


  • las características son lanzadas por otro archivo, Features.java usando la anotación RunWith de JUnit 4, Cucumber no es compatible con la versión 5, por desgracia
  • @SpringBootTest anotación @SpringBootTest se agrega a la descripción de los pasos, cucumber-spring recoge desde allí y configura el contexto de prueba (es decir, inicia la aplicación)
  • La aplicación Spring para la prueba comienza con webEnvironment = RANDOM_PORT y este puerto aleatorio se pasa a la prueba usando @LocalServerPort , Spring encontrará esta anotación y establecerá el valor del campo en el puerto del servidor

Y la prueba, como se esperaba, se bloquea con el error 404 for http://localhost:51517 .


Los errores con los que se bloquea la prueba son increíblemente importantes, especialmente cuando se trata de pruebas unitarias o de integración, y estos errores son parte de la API. Si la prueba falla con una NullPointerException esto no es demasiado bueno, pero la BaseUrl configuration property is not set , mucho mejor.

Verde


Para que la prueba sea verde, agregué un controlador base y una vista con HTML mínimo:


 @Controller public class IndexController { @GetMapping public String index() { return "index"; } } 

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Cake Factory</title> </head> <body> <h1>Thank you for your interest</h1> <h2>The web-site is coming in December!</h2> </body> </html> 

La prueba es verde, la aplicación funciona, aunque se realiza en la tradición del diseño de ingeniería severa.


En un proyecto real y en un equipo equilibrado , por supuesto, me sentaría con el diseñador y convertiríamos el HTML desnudo en algo mucho más hermoso. Pero dentro del marco del artículo, no ocurrirá un milagro, la princesa seguirá siendo una rana.

La pregunta "qué parte de TDD es diseño" no es tan simple. Una de las prácticas que encontré útiles es al principio ni siquiera mirar la interfaz de usuario (ni siquiera ejecutar la aplicación para salvar los nervios), escribir una prueba, hacerla verde, y luego, tener una base estable, trabajar en el front-end, reiniciar constantemente las pruebas .


Refactor


En la primera iteración, no hay una refactorización particular, pero aunque pasé los últimos 10 minutos eligiendo una plantilla para Bulma , ¡que puede contarse como refactorización!


En conclusión


Si bien la aplicación no tiene trabajo de seguridad, ni una base de datos, ni una API, las pruebas y los TDD parecen bastante simples. Y en general, desde la pirámide de prueba, toqué solo la parte superior, la prueba de IU. Pero en esto, en parte, el secreto del enfoque lean es hacer todo en pequeñas iteraciones, un componente a la vez. Esto ayuda a centrarse en las pruebas, simplificarlas y controlar la calidad del código. Espero que en los siguientes artículos haya más interesante.


Referencias



PD: El título del artículo no es tan loco como podría parecer al principio, creo que muchos ya lo han adivinado. "Cómo construir una pirámide en tu bota" es una referencia a la pirámide de prueba (te contaré más sobre esto más adelante) y Spring Boot, donde arrancar en inglés británico también significa "tronco".

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


All Articles