
Al llegar a un nuevo proyecto, me encuentro regularmente con una de las siguientes situaciones:
- No hay pruebas en absoluto.
- Hay pocas pruebas, rara vez se escriben y no se ejecutan de forma continua.
- Las pruebas están presentes e incluidas en CI (integración continua), pero hacen más daño que bien.
Desafortunadamente, es el último escenario el que a menudo conduce a intentos serios de comenzar a implementar pruebas en ausencia de las habilidades apropiadas.
¿Qué se puede hacer para cambiar la situación actual? La idea de usar pruebas no es nueva. Al mismo tiempo, la mayoría de los tutoriales se asemejan a la famosa imagen sobre cómo dibujar un búho: conecta JUnit, escribe la primera prueba, usa el primer simulacro, ¡y listo! Dichos artículos no responden preguntas sobre qué pruebas deben escribirse, a qué vale la pena prestar atención y cómo vivir con todo esto. De aquí nació la idea de este artículo. Traté de resumir brevemente mi experiencia en la implementación de pruebas en diferentes proyectos para facilitar este camino para todos.

Hay más que suficientes artículos introductorios sobre este tema, por lo que no nos repetiremos e intentaremos ir desde el otro lado. En la primera parte, desacreditaremos el mito de que las pruebas conllevan costos exclusivamente adicionales. Se mostrará cómo la creación de pruebas de calidad a su vez puede acelerar el proceso de desarrollo. Luego, en el ejemplo de un proyecto pequeño, se considerarán los principios y reglas básicos que deben seguirse para obtener este beneficio. Finalmente, en la sección final, se darán recomendaciones de implementación específicas: cómo evitar problemas típicos cuando comienzan las pruebas, por el contrario, ralentiza significativamente el desarrollo.
Dado que mi especialización principal es el backend de Java, se utilizará la siguiente pila de tecnología en los ejemplos: Java, JUnit, H2, Mockito, Spring, Hibernate. Al mismo tiempo, una parte importante del artículo está dedicada a problemas generales de prueba y los consejos que contiene son aplicables a una gama mucho más amplia de tareas.
Sin embargo, ten cuidado! Las pruebas son muy adictivas: una vez que aprende a usarlas, ya no puede vivir sin ellas.
Pruebas vs velocidad de desarrollo
Las principales preguntas que surgen al discutir la implementación de las pruebas: ¿cuánto tiempo tomará escribir las pruebas y qué beneficios tendrá? Las pruebas, como cualquier otra tecnología, requerirán serios esfuerzos para el desarrollo y la implementación, por lo que al principio no deben esperarse beneficios significativos. En cuanto a los costos de tiempo, dependen mucho del equipo en particular. Sin embargo, menos del 20-30% de los costos adicionales de codificación no deben calcularse exactamente. Menos simplemente no es suficiente para lograr al menos algún resultado. La expectativa de retornos instantáneos es a menudo la razón principal para reducir esta actividad incluso antes de que las pruebas sean útiles.
¿Pero de qué tipo de eficiencia estamos hablando? Dejemos caer la letra sobre las dificultades de implementación y veamos qué oportunidades específicas se abren para ahorrar tiempo en las pruebas.
Ejecutando código en cualquier lugar
Si no hay pruebas en el proyecto, la única forma de comenzar es levantar toda la aplicación. Es bueno si demora entre 15 y 20 segundos, pero los casos de proyectos grandes en los que un lanzamiento completo puede demorar varios minutos están lejos de ser raros. ¿Qué significa esto para los desarrolladores? Una parte importante de su tiempo de trabajo serán estas breves sesiones de espera, durante las cuales es imposible continuar trabajando en la tarea actual, pero al mismo tiempo hay muy poco tiempo para cambiar a otra cosa. Muchos han encontrado al menos una vez proyectos de este tipo en los que el código escrito en una hora requiere muchas horas de depuración debido a largos reinicios entre correcciones. En las pruebas, puede limitarse a ejecutar pequeñas partes de la aplicación, lo que reducirá significativamente el tiempo de espera y aumentará la productividad de trabajar en el código.
Además, la capacidad de ejecutar código en cualquier lugar conduce a una depuración más completa. A menudo, verificar incluso los principales casos de uso positivo a través de la interfaz de la aplicación requiere mucho esfuerzo y tiempo. La presencia de pruebas permite realizar una verificación detallada de un funcional específico mucho más fácil y rápido.
Otra ventaja es la capacidad de regular el tamaño de la unidad probada. Dependiendo de la complejidad de la lógica que se está probando, puede restringirse a un método, una clase, un grupo de clases que implementen alguna funcionalidad, un servicio, etc., hasta la automatización de probar toda la aplicación. Esta flexibilidad le permite descargar pruebas de alto nivel de muchas partes debido al hecho de que se probarán en niveles más bajos.
Relanzar pruebas
Este plus a menudo se cita como la esencia de la automatización de pruebas, pero veámoslo desde un ángulo menos familiar. ¿Qué nuevas oportunidades abre para los desarrolladores?
En primer lugar, cada nuevo desarrollador que participó en el proyecto podrá ejecutar fácilmente las pruebas existentes para comprender la lógica de la aplicación utilizando ejemplos. Desafortunadamente, la importancia de esto se subestima enormemente. En las condiciones modernas, las mismas personas rara vez trabajan en un proyecto durante más de 1-2 años. Y dado que los equipos están formados por varias personas, la aparición de un nuevo participante cada 2-3 meses es una situación típica para proyectos relativamente grandes. ¡Los proyectos particularmente difíciles están experimentando cambios de generaciones enteras de desarrolladores! La capacidad de iniciar fácilmente cualquier parte de la aplicación y observar el comportamiento del sistema a veces simplifica la inmersión de nuevos programadores en el proyecto. Además, un estudio más detallado de la lógica del código reduce la cantidad de errores cometidos en la salida y el tiempo para depurarlos en el futuro.
En segundo lugar, la capacidad de verificar fácilmente que la aplicación funciona correctamente abre el camino para la Refactorización Continua. Este término, desafortunadamente, es mucho menos popular que CI. Significa que la refactorización puede y debe hacerse cada vez que se refina el código. Es la observancia regular de la notoria regla de Boy Scout "dejar el estacionamiento más limpio de lo que estaba antes de su llegada", lo que permite evitar la degradación de la base del código y garantiza al proyecto una vida larga y feliz.
Depuración
La depuración ya se ha mencionado en los párrafos anteriores, pero este punto es tan importante que merece una mirada más cercana. Desafortunadamente, no hay una manera confiable de medir la relación entre el tiempo dedicado a escribir código y depurarlo, ya que estos procesos son prácticamente inseparables entre sí. Sin embargo, la presencia de pruebas de calidad en el proyecto reduce significativamente el tiempo de depuración, hasta la ausencia casi completa de la necesidad de ejecutar un depurador.
Efectividad
Todo lo anterior puede ahorrar significativamente tiempo en la depuración inicial del código. Con el enfoque correcto, solo esto pagará todos los costos de desarrollo adicionales. Las bonificaciones de prueba restantes: mejorar la calidad de la base del código (el código mal diseñado es difícil de probar), reducir la cantidad de defectos, la capacidad de verificar la corrección del código en cualquier momento, etc., será casi gratis.
De la teoría a la práctica.
En palabras, todo se ve bien, pero vamos al grano. Como se mencionó anteriormente, hay materiales más que suficientes sobre cómo hacer la configuración inicial del entorno de prueba. Por lo tanto, procedemos inmediatamente al proyecto terminado.
Fuentes aquí.Desafío
Como una tarea de plantilla, considere un pequeño fragmento del backend de una tienda en línea. Escribiremos una API típica para trabajar con productos: creación, recepción, edición. Además de un par de métodos para trabajar con clientes: cambiar un "producto favorito" y calcular puntos de bonificación para un pedido.
Modelo de dominio
Para no sobrecargar el ejemplo, nos restringimos a un conjunto mínimo de campos y clases.
El cliente tiene un nombre de usuario, un enlace a un producto favorito y una bandera que indica si es un cliente premium.
Producto (Producto): nombre, precio, descuento y bandera que indica si se anuncia actualmente.
Estructura del proyecto
La estructura del código principal del proyecto es la siguiente.
Las clases son en capas:
- Modelo: modelo de dominio del proyecto;
- Jpa: repositorios para trabajar con bases de datos basadas en Spring Data;
- Servicio - lógica de negocios de la aplicación;
- Controlador: controladores que implementan la API.
Estructura de prueba unitaria.
Las clases de prueba están en los mismos paquetes que el código original. Además, se creó un paquete con creadores para la preparación de datos de prueba, pero más sobre eso a continuación.
Es conveniente separar las pruebas unitarias y las pruebas de integración. A menudo tienen dependencias diferentes, y para un desarrollo cómodo, debería existir la capacidad de ejecutar uno u otro. Esto se puede lograr de varias maneras: convenciones de nombres, módulos, paquetes, conjuntos de fuentes. La elección de un método específico es exclusivamente una cuestión de gustos. En este proyecto, las pruebas de integración se encuentran en un conjunto de fuentes separado: prueba de integración.
Al igual que las pruebas unitarias, las clases con pruebas de integración están en los mismos paquetes que el código original. Además, hay clases base que ayudan a eliminar la duplicación de la configuración y, si es necesario, contienen métodos universales útiles.
Pruebas de integración
Existen diferentes enfoques con respecto a qué pruebas vale la pena comenzar. Si la lógica probada no es muy complicada, puede pasar inmediatamente a las de integración (a veces también se las denomina aceptación). A diferencia de las pruebas unitarias, se aseguran de que la aplicación en su conjunto funcione correctamente.
ArquitecturaPrimero debe decidir en qué nivel específico se realizarán las comprobaciones de integración. Spring Boot proporciona total libertad de elección: puede plantear parte del contexto, todo el contexto e incluso un servidor completo, accesible desde las pruebas. A medida que aumenta el tamaño de la aplicación, este problema se vuelve cada vez más complejo. A menudo tienes que escribir diferentes pruebas en diferentes niveles.
Un buen punto de partida serían las pruebas del controlador sin iniciar el servidor. En aplicaciones relativamente pequeñas, es bastante aceptable plantear todo el contexto, ya que por defecto se reutiliza entre pruebas y se inicializa solo una vez. Considere los métodos básicos de la clase
ProductController
:
@PostMapping("new") public Product createProduct(@RequestBody Product product) { return productService.createProduct(product); } @GetMapping("{productId}") public Product getProduct(@PathVariable("productId") long productId) { return productService.getProduct(productId); } @PostMapping("{productId}/edit") public void updateProduct(@PathVariable("productId") long productId, @RequestBody Product product) { productService.updateProduct(productId, product); }
La cuestión del manejo de errores se deja de lado. Supongamos que se implementa externamente en función de un análisis de excepciones lanzadas. El código de los métodos es muy simple, su implementación en
ProductService
no
ProductService
mucho más complicada:
@Transactional(readOnly = true) public Product getProduct(Long productId) { return productRepository.findById(productId) .orElseThrow(() -> new DataNotFoundException("Product", productId)); } @Transactional public Product createProduct(Product product) { return productRepository.save(new Product(product)); } @Transactional public Product updateProduct(Long productId, Product product) { Product dbProduct = productRepository.findById(productId) .orElseThrow(() -> new DataNotFoundException("Product", productId)); dbProduct.setPrice(product.getPrice()); dbProduct.setDiscount(product.getDiscount()); dbProduct.setName(product.getName()); dbProduct.setIsAdvertised(product.isAdvertised()); return productRepository.save(dbProduct); }
El repositorio de
ProductRepository
no contiene sus propios métodos:
public interface ProductRepository extends JpaRepository<Product, Long> { }
Todo indica que estas clases no necesitan pruebas unitarias simplemente porque toda la cadena se puede verificar de manera fácil y eficiente mediante varias pruebas de integración. La duplicación de las mismas pruebas en diferentes pruebas complica la depuración. En el caso de un error en el código, ahora no caerá una sola prueba, sino 10-15 a la vez. Esto a su vez requerirá más análisis. Si no hay duplicación, es probable que la única prueba caída indique inmediatamente un error.
ConfiguracionPara mayor comodidad, destacamos la clase base
BaseControllerIT
, que contiene la configuración Spring y un par de campos:
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @Transactional public abstract class BaseControllerIT { @Autowired protected ProductRepository productRepository; @Autowired protected CustomerRepository customerRepository; }
Los repositorios se mueven a la clase base para no saturar las clases de prueba. Su función es exclusivamente auxiliar: preparar datos y verificar el estado de la base de datos después de que el controlador funciona. Cuando aumenta el tamaño de la aplicación, puede que esto ya no sea conveniente, pero para empezar es bastante adecuado.
La configuración principal de Spring está definida por las siguientes líneas:
@SpringBootTest
: se usa para establecer el contexto de la aplicación.
WebEnvironment.NONE
significa que no es necesario
WebEnvironment.NONE
contexto web.
@Transactional
: envuelve todas las pruebas de clase en una transacción con reversión automática para guardar el estado de la base de datos.
Estructura de pruebaPasemos a un conjunto minimalista de pruebas para la clase
ProductControllerIT
:
ProductControllerIT
.
@Test public void createProduct_productSaved() { Product product = product("productName").price("1.01").discount("0.1").advertised(true).build(); Product createdProduct = productController.createProduct(product); Product dbProduct = productRepository.getOne(createdProduct.getId()); assertEquals("productName", dbProduct.getName()); assertEquals(number("1.01"), dbProduct.getPrice()); assertEquals(number("0.1"), dbProduct.getDiscount()); assertEquals(true, dbProduct.isAdvertised()); }
El código de prueba debe ser extremadamente simple y comprensible de un vistazo. Si esto no es así, se pierden la mayoría de las ventajas de las pruebas descritas en la primera sección del artículo. Es una buena práctica dividir el cuerpo de prueba en tres partes que se pueden separar visualmente entre sí: preparar datos, llamar al método de prueba, validar los resultados. Al mismo tiempo, es muy deseable que el código de prueba se ajuste a toda la pantalla.
Personalmente, me parece más obvio cuando los valores de prueba de la sección de preparación de datos se usan más adelante en las verificaciones. Alternativamente, podría comparar objetos explícitamente, por ejemplo, así:
assertEquals(product, dbProduct);
En otra prueba para actualizar la información del producto (
updateProduct
), está claro que la creación de datos se ha vuelto un poco más complicada y para mantener la integridad visual de las tres partes de la prueba, están separadas por dos avances de línea seguidos:
@Test public void updateProduct_productUpdated() { Product product = product("productName").build(); productRepository.save(product); Product updatedProduct = product("updatedName").price("1.1").discount("0.5").advertised(true).build(); updatedProduct.setId(product.getId()); productController.updateProduct(product.getId(), updatedProduct); Product dbProduct = productRepository.getOne(product.getId()); assertEquals("updatedName", dbProduct.getName()); assertEquals(number("1.1"), dbProduct.getPrice()); assertEquals(number("0.5"), dbProduct.getDiscount()); assertEquals(true, dbProduct.isAdvertised()); }
Cada una de las tres partes de la masa se puede simplificar. Para la preparación de datos, los creadores de pruebas son excelentes, que contienen la lógica para crear objetos que es conveniente para usar desde las pruebas. Las llamadas a métodos demasiado complejos se pueden convertir en métodos auxiliares dentro de las clases de prueba, ocultando algunos de los parámetros que son irrelevantes para esta clase. Para simplificar las comprobaciones complejas, también puede escribir funciones auxiliares o implementar sus propios comparadores. Lo principal con todas estas simplificaciones es no perder la visibilidad de la prueba: todo debe quedar claro de un vistazo al método principal, sin la necesidad de profundizar.
Constructores de pruebaLos fabricantes de pruebas merecen especial atención. Encapsular la lógica de crear objetos simplifica el mantenimiento de la prueba. En particular, el relleno de los campos del modelo que no son relevantes para esta prueba se puede ocultar dentro del generador. Para hacer esto, no tiene que crearlo directamente, sino que utiliza un método estático que completará los campos faltantes con los valores predeterminados. Por ejemplo, si aparecen nuevos campos obligatorios en el modelo, se pueden agregar fácilmente a este método. En
ProductBuilder
se ve así:
public static ProductBuilder product(String name) { return new ProductBuilder() .name(name) .advertised(false) .price("0.00"); }
Nombre de la pruebaEs imprescindible comprender lo que se prueba específicamente en esta prueba. Para mayor claridad, es mejor dar una respuesta a esta pregunta en su título. Usando las pruebas de muestra para el método
getProduct
considere la convención de nomenclatura utilizada:
@Test public void getProduct_oneProductInDb_productReturned() { Product product = product("productName").build(); productRepository.save(product); Product result = productController.getProduct(product.getId()); assertEquals("productName", result.getName()); } @Test public void getProduct_twoProductsInDb_correctProductReturned() { Product product1 = product("product1").build(); Product product2 = product("product2").build(); productRepository.save(product1); productRepository.save(product2); Product result = productController.getProduct(product1.getId()); assertEquals("product1", result.getName()); }
En el caso general, el título del método de prueba consta de tres partes, separadas por subrayado: el nombre del método que se está probando, el script y el resultado esperado. Sin embargo, nadie canceló el sentido común, y puede estar justificado omitir algunas partes del nombre si no son necesarias en este contexto (por ejemplo, un script en una sola prueba para crear un producto). El propósito de este nombramiento es asegurar que la esencia de cada prueba sea comprensible sin aprender el código. Esto hace que la ventana de resultados de las pruebas sea lo más clara posible, y es con ella que generalmente comienza el trabajo con las pruebas.
ConclusionesEso es todo Por primera vez, un conjunto mínimo de cuatro pruebas es suficiente para probar los métodos de la clase
ProductController
. En caso de detección de errores, siempre puede agregar las pruebas que faltan. Al mismo tiempo, el número mínimo de pruebas reduce significativamente el tiempo y el esfuerzo para apoyarlas. Esto, a su vez, es crítico en el proceso de implementación de las pruebas, ya que las primeras pruebas generalmente no se obtienen con la mejor calidad y crean muchos problemas inesperados. Al mismo tiempo, este conjunto de pruebas es suficiente para recibir los bonos descritos en la primera parte del artículo.
Vale la pena señalar que tales pruebas no verifican la capa web de la aplicación, pero a menudo esto no es obligatorio. Si es necesario, puede escribir pruebas separadas para la capa web con un código auxiliar en lugar de la base (
@WebMvcTest
,
MockMvc
,
@MockBean
) o utilizar un servidor completo. Este último puede complicar la depuración y complicar el trabajo con las transacciones, ya que la prueba no puede controlar la transacción del servidor. Un ejemplo de dicha prueba de integración se puede encontrar en la clase
CustomerControllerServerIT
.
Pruebas unitarias
Las pruebas unitarias tienen varias ventajas sobre las pruebas de integración:
- La puesta en marcha lleva milisegundos;
- Pequeño tamaño de la unidad probada;
- Es fácil implementar la verificación de una gran cantidad de opciones, ya que cuando se llama directamente al método, la preparación de datos se simplifica enormemente.
A pesar de esto, las pruebas unitarias por su naturaleza no pueden garantizar la operatividad de la aplicación en su conjunto y no le permiten evitar escribir las de integración. Si la lógica de la unidad bajo prueba es simple, la duplicación de las pruebas de integración con las pruebas unitarias no traerá ningún beneficio, sino que solo agregará más código para soportar.
La única clase en este ejemplo que merece pruebas unitarias es
BonusPointCalculator
. Su característica distintiva es una gran cantidad de ramas de la lógica empresarial. Por ejemplo, se supone que el comprador recibe bonos del 10% del costo del producto, multiplicado por no más de 2 multiplicadores de la siguiente lista:
- El producto cuesta más de 10,000 (× 4);
- El producto participa en una campaña publicitaria (× 3);
- El producto es el producto "favorito" del cliente (× 5);
- El cliente tiene un estado premium (× 2);
- Si el cliente tiene un estado premium y compra un producto "favorito", en lugar de los dos multiplicadores indicados, se utiliza uno (× 8).
En la vida real, por supuesto, valdría la pena diseñar un mecanismo universal flexible para calcular estos bonos, pero para simplificar el ejemplo, nos restringimos a una implementación fija. El código de cálculo del multiplicador se ve así:
private List<BigDecimal> calculateMultipliers(Customer customer, Product product) { List<BigDecimal> multipliers = new ArrayList<>(); if (customer.getFavProduct() != null && customer.getFavProduct().equals(product)) { if (customer.isPremium()) { multipliers.add(PREMIUM_FAVORITE_MULTIPLIER); } else { multipliers.add(FAVORITE_MULTIPLIER); } } else if (customer.isPremium()) { multipliers.add(PREMIUM_MULTIPLIER); } if (product.isAdvertised()) { multipliers.add(ADVERTISED_MULTIPLIER); } if (product.getPrice().compareTo(EXPENSIVE_THRESHOLD) >= 0) { multipliers.add(EXPENSIVE_MULTIPLIER); } return multipliers; }
Una gran cantidad de opciones lleva al hecho de que dos o tres pruebas de integración no están limitadas aquí. Un conjunto minimalista de pruebas unitarias es perfecto para depurar dicha funcionalidad.
El conjunto de pruebas correspondiente se puede encontrar en la clase
BonusPointCalculatorTest
. Aquí hay algunos de ellos:
@Test public void calculate_oneProduct() { Product product = product("product").price("1.00").build(); Customer customer = customer("customer").build(); Map<Product, Long> quantities = mapOf(product, 1L); BigDecimal bonus = bonusPointCalculator.calculate(customer, list(product), quantities::get); BigDecimal expectedBonus = bonusPoints("0.10").build(); assertEquals(expectedBonus, bonus); } @Test public void calculate_favProduct() { Product product = product("product").price("1.00").build(); Customer customer = customer("customer").favProduct(product).build(); Map<Product, Long> quantities = mapOf(product, 1L); BigDecimal bonus = bonusPointCalculator.calculate(customer, list(product), quantities::get); BigDecimal expectedBonus = bonusPoints("0.10").addMultiplier(FAVORITE_MULTIPLIER).build(); assertEquals(expectedBonus, bonus); }
Vale la pena señalar que en las pruebas nos referimos específicamente a la API pública de la clase: el método de
calculate
. Probar un contrato de clase en lugar de su implementación evita el colapso de las pruebas debido a cambios no funcionales y refactorización.
Finalmente, cuando verificamos la lógica interna con pruebas unitarias, ya no necesitamos poner todos estos detalles en integración. En este caso, una prueba más o menos representativa es suficiente, por ejemplo esto:
@Test public void calculateBonusPoints_twoProductTypes_correctValueCalculated() { Product product1 = product("product1").price("1.01").build(); Product product2 = product("product2").price("10.00").build(); productRepository.save(product1); productRepository.save(product2); Customer customer = customer("customer").build(); customerRepository.save(customer); Map<Long, Long> quantities = mapOf(product1.getId(), 1L, product2.getId(), 2L); BigDecimal bonus = customerController.calculateBonusPoints( new CalculateBonusPointsRequest("customer", quantities) ); BigDecimal bonusPointsProduct1 = bonusPoints("0.10").build(); BigDecimal bonusPointsProduct2 = bonusPoints("1.00").quantity(2).build(); BigDecimal expectedBonus = bonusPointsProduct1.add(bonusPointsProduct2); assertEquals(expectedBonus, bonus); }
Como en el caso de las pruebas de integración, el conjunto de pruebas unitarias utilizado es muy pequeño y no garantiza la corrección completa de la aplicación. Sin embargo, su presencia aumenta significativamente la confianza en el código, facilita la depuración y otorga los otros bonos enumerados en la primera parte del artículo.
Recomendaciones de implementación
Espero que las secciones anteriores hayan sido suficientes para convencer al menos a un desarrollador de que intente comenzar a usar pruebas en su proyecto. Este capítulo enumerará brevemente las principales recomendaciones que ayudarán a evitar problemas graves y reducirán los costos iniciales de implementación.
Intente comenzar a implementar las pruebas en la nueva aplicación. Escribir las primeras pruebas en un gran proyecto heredado será mucho más difícil y requerirá más habilidad que en uno recién creado. Por lo tanto, si es posible, es mejor comenzar con una nueva aplicación pequeña. Si no se esperan nuevas aplicaciones completas, puede intentar desarrollar alguna utilidad útil para uso interno. Lo principal es que la tarea debe ser más o menos realista: los ejemplos inventados no proporcionarán una experiencia completa.
Configure ejecuciones de prueba regulares. Si las pruebas no se ejecutan de manera regular, no solo dejan de realizar su función principal, verificar la corrección del código, sino que también quedan desactualizadas rápidamente. Por lo tanto, es extremadamente importante configurar al menos la canalización mínima de CI con el inicio automático de las pruebas cada vez que el código se actualiza en el repositorio.
No persigas la tapa. Como en el caso de cualquier otra tecnología, al principio las pruebas no se obtendrán de la mejor calidad. La literatura relevante (enlaces al final del artículo) o un mentor competente pueden ayudar aquí, pero esto no cancela la necesidad de conos de relleno. Las pruebas a este respecto son similares al resto del código: para comprender cómo afectarán el proyecto, solo puede hacerlo después de vivir con ellos durante un tiempo. Por lo tanto, para minimizar el daño, la primera vez es mejor no perseguir el número y los números hermosos como una cobertura del cien por ciento. En cambio, debe limitarse a los principales escenarios positivos para la funcionalidad de su propia aplicación.
No se deje llevar por las pruebas unitarias. Continuando con el tema de "cantidad versus calidad", debe tenerse en cuenta que las pruebas unitarias honestas no deben llevarse a cabo por primera vez, porque esto puede conducir fácilmente a una especificación excesiva de la aplicación. A su vez, esto se convertirá en un factor inhibidor serio en la refactorización posterior y las mejoras de la aplicación. Las pruebas unitarias solo deben usarse si hay una lógica compleja en una clase particular o grupo de clases, lo cual es inconveniente para verificar a nivel de integración.
No se deje llevar por las clases de código auxiliar y los métodos de aplicación. Stubs (stub, simulacro) es otra herramienta que requiere un enfoque equilibrado y mantener un equilibrio. Por un lado, el aislamiento completo de la unidad le permite concentrarse en la lógica probada y no pensar en el resto del sistema. Por otro lado, esto requerirá un tiempo de desarrollo adicional y, como con las pruebas unitarias, puede conducir a una especificación excesiva de comportamiento.
Desatar las pruebas de integración de sistemas externos. Un error muy común en las pruebas de integración es el uso de una base de datos real, colas de mensajes y otros sistemas externos a la aplicación. Por supuesto, la capacidad de ejecutar una prueba en un entorno real es útil para la depuración y el desarrollo. Tales pruebas en pequeñas cantidades pueden tener sentido, especialmente para correr de forma interactiva. Sin embargo, su uso generalizado genera varios problemas:
- Para ejecutar las pruebas, deberá configurar el entorno externo. Por ejemplo, instale una base de datos en cada máquina donde se ensamblará la aplicación. Esto dificultará que los nuevos desarrolladores ingresen al proyecto y configuren CI.
- El estado de los sistemas externos puede variar en diferentes máquinas antes de ejecutar las pruebas. Por ejemplo, la base de datos ya puede contener las tablas que la aplicación necesita con datos que no se esperan en la prueba. Esto conducirá a fallas impredecibles en las pruebas, y su eliminación requerirá una cantidad significativa de tiempo.
- Si se está trabajando en paralelo en varios proyectos, es posible la influencia no obvia de algunos proyectos en otros. Por ejemplo, la configuración específica de la base de datos realizada para uno de los proyectos puede ayudar a que la funcionalidad de otro proyecto funcione correctamente, lo que, sin embargo, se romperá cuando se inicie en una base de datos limpia en otra máquina.
- Las pruebas se llevan a cabo durante mucho tiempo: una ejecución completa puede alcanzar decenas de minutos. Esto lleva al hecho de que los desarrolladores dejan de ejecutar pruebas localmente y miran sus resultados solo después de enviar los cambios al repositorio remoto. Este comportamiento niega la mayoría de las ventajas de las pruebas, que se discutieron en la primera parte del artículo.
Borrar el contexto entre las pruebas de integración. A menudo, para acelerar el trabajo de las pruebas de integración, debe reutilizar el mismo contexto entre ellas. Incluso la documentación oficial de Spring hace tal recomendación. Al mismo tiempo, se debe evitar la influencia de las pruebas entre sí. Dado que se lanzan en un orden arbitrario, la presencia de tales conexiones puede conducir a errores aleatorios irreproducibles. Para evitar que esto suceda, las pruebas no deben dejar atrás ningún cambio en el contexto. Por ejemplo, cuando se usa una base de datos, para el aislamiento, generalmente es suficiente revertir todas las transacciones confirmadas en la prueba. Si no se pueden evitar los cambios en el contexto, puede configurar su recreación utilizando la anotación
@DirtiesContext
.
, . , - . , . , , — , .
. , , . , , .
TDD (Test-Driven Development). TDD , , . , , . , , .
, ?
, :
- ( )? .
- , ( , CI)? .
- ? .
- ? . , , .
, . , , - . — .
Conclusión
, . - , . , - . — , , -. , .
, , , !
GitHub