Skip to content

La filosofía de ingeniería de Red Sift: Prueba rápido, prueba a fondo

El equipo de ingeniería de Red Sift comparte cómo abordamos las pruebas de software: qué significa en la práctica probar rápido y a fondo, y por qué esto hace que el producto sea más fiable.

Red Sift
Published: April 30, 2019·Updated: March 18, 2024·14 min read

Una versión de este blog fue publicada originalmente por Test MagazineTest Magazine en marzo de 2019, ¡la compartimos de nuevo para quienes se la perdieron la primera vez!

Developers like flame wars. Last Friday, when the afternoon was slowing in the office, an inadvertent GIF on Slack sparked some friendly debate about the right way to test. Because winning flame wars is important, here is my “well, actually”… 

En algún momento de la década de 2000, cuando PHP ya no se consideraba solo un lenguaje de plantillas y Ruby acababa de subirse a Rails, la comunidad de programación decidió que los lenguajes de tipado dinámico eran una excelente manera de reducir la carga cognitiva del programador, mantenerse implacablemente pragmáticos y evitar la cultura tipo fábrica de Java.

El mantra de Facebook de “Muévete rápido y rompe cosas” se ha perpetuado desde entonces por toda la escena de startups, y se considera la forma de construir negocios, productos, casas o cualquier cosa, en realidad, que pueda derrumbarse sobre ti. Y si alguna vez has trabajado en software, sabes que lo que puede pasar, pasará.

En algún punto del camino llegó Clean Code, y el auge del TDD reunió a una multitud en torno a Escribir Pruebas Primero, la creencia en La Cobertura, y otros mantras que son divertidos de recitar, pero significativamente menos divertidos de hacer.

Muchos libros de autoayuda como Clean Code se basan vagamente en la experiencia personal, y traen ciertos patrones de programación a dominios donde tradicionalmente no se habían usado: por ejemplo, simplificar la orientación a objetos al estilo C++ con algunos conceptos de programación funcional como funciones pequeñas, simples y puras.

En realidad, ha habido mucha investigación sobre la crisis del software y cómo salir del lío en el que estamos, y a menudo contradice la sabiduría popular. Echemos un vistazo a diferentes estrategias que impulsan la calidad del software, y dónde realmente marcan la diferencia.

De abajo hacia arriba, generalmente examino la verificación del software en las siguientes capas: sistema de tipos, pruebas unitarias, pruebas de integración y estructura de gestión organizacional.

¿Espera, qué? ¿Estructura de gestión organizacional? Bueno, quizás podamos empezar por ahí.

Gestión

El comportamiento organizacional es una ciencia social por derecho propio, y estudia el sutil arte de las estructuras de gestión de personas. Dado que el software normalmente lo hacen humanos, estos tienen necesidades, motivadores internos y externos, y ocasionalmente necesitan trabajar juntos para entregar algún resultado.

“quantifiable results from the study show that team and collaboration structure can be a better predictor of quality than tooling, testing strategies, or other code-based metrics.” 

Google realizó un estudio sobre sus equipos como unidades de trabajo para identificar qué los hacía más eficaces, pero la investigación de Microsoft se centró en cómo la estructura organizacional determinaba las tasas de fallos del software. Ambos son enfoques interesantes a su manera, y el estudio de Microsoft nos ofrece una visión interesante del desarrollo de Windows Vista. El estudio nos dice que los equipos más pequeños y enfocados producen software más fiable. La alta rotación de ingenieros reduce la calidad del software, mientras que una colaboración más estrecha entre equipos que trabajan en el mismo proyecto resultará en menores tasas de fallos.

Esto podría parecer una afirmación del Capitán Obvio (o de Melvin Conway), sin embargo, los resultados cuantificables del estudio muestran que la estructura de equipo y colaboración puede ser un mejor predictor de calidad que las herramientas, las estrategias de prueba u otras métricas basadas en código.

La forma en que cualquier equipo individual controla la calidad de su producción es el siguiente paso a partir de aquí. Las revisiones de código en particular son una excelente manera de crear y mantener un conjunto común de estándares. Las revisiones de código escritas obligan a los ingenieros a comunicar sus inquietudes con claridad, y esta mayor comunicación técnica ayudará a todos en el equipo a aprender sobre diferentes estilos y perspectivas, y al mismo tiempo ayudará a nivelar las habilidades en todo el equipo. Aunque quizás sea mejor evitar el estilo de Linus Torvalds.

Pruebas de integración

Las pruebas de integración, sorpresa sorpresa, prueban la integración de componentes o módulos en un sistema. También puedes probar la integración de módulos integrados, y hay tortugas hasta el fondo.

A menudo es más fácil escribir código correcto de forma aislada, por lo que una gran cantidad de errores ocurren en los límites del sistema. Validar entradas y formatear salidas, no verificar los niveles de permisos, o una mala implementación de los esquemas de interfaz. Este problema se ve amplificado por la tendencia actual de los microservicios, donde las versiones de interfaz pueden desincronizarse entre varios servicios dentro del sistema.

En este nivel, lo mejor es escribir pruebas de extremo a extremo tipo pass-through para las funcionalidades, y tratar de aprovechar que tenemos tantas otras capas de protección contra fallos, de modo que algo eventualmente active esas alarmas. De hecho, se ha demostrado que la cobertura de código en las pruebas de integración no es un indicador fiable de las tasas de fallos.

Si lo miras de otra manera, la producción es simplemente una gran prueba de integración. Un truco que me encanta hacer es crear una instancia de producción muda, que recibe una parte del tráfico real de producción, pero nunca generará respuestas para los usuarios. Con suficiente inversión en una capa de orquestación sin estado, incluso podemos probar en modo mudo subárboles de servicios en lugares estratégicos, luego activarlos y descartar el subárbol antiguo una vez que la carga de trabajo haya desaparecido.

Combinado con los principios detrás de la construcción de sistemas altamente observables, este tipo de entorno de pruebas elimina mucha ansiedad sobre lo que sucede cuando desplegamos a producción, porque el mute-prod recibirá exactamente los mismos datos. Cuantos más controles y sondas expongamos en los sistemas en vivo, mejor visibilidad obtendremos de los aspectos internos.

Pruebas unitarias

Entonces, para integrar módulos, queremos tener cierta confianza en que los módulos en sí funcionan según lo especificado. Aquí es donde entran en escena las pruebas unitarias.

Las pruebas unitarias suelen ser rápidas, y más o menos exhaustivas para piezas aisladas de bloques de construcción fáciles de comprender. ¿Qué tan rápido? El repositorio master de Ruby on Rails ejecuta aproximadamente 67 pruebas y 176 aserciones por segundo.

Como regla general, una prueba debería cubrir un escenario que puede ocurrirle a un módulo. En comparación con las pruebas de integración, el mismo estudio de Niedermayr, Juergens y Wagner muestra que la cobertura de código a nivel de pruebas unitarias sí influye en las tasas de fallos, si se hace bien.

Un estudio del 94 realizado por Hutchins et al. afirma que los niveles de cobertura superiores al 90% mostraron mejores tasas de detección de fallos que conjuntos de pruebas más pequeños, y que se produjeron mejoras “significativas” a medida que la cobertura aumentaba del 90% al 100%.

El movimiento BDD tiene esta divertida práctica de desarrollar especificaciones y convertirlas directamente en pruebas unitarias. Los beneficios de tener pruebas unitarias claras y legibles por humanos ayudan a documentar el código y facilitan parte de la transferencia de conocimiento que debe ocurrir cuando los desarrolladores inevitablemente van y vienen, o cuando cambian los requisitos de los componentes existentes.

Las pruebas unitarias, en mi opinión, también incluyen las pruebas generadas al estilo QuickCheck. La idea de QuickCheck es que, en lugar de tener algún código imperativo del tipo si-esto-entonces-aquello para recorrer, el programador puede enumerar suposiciones que deben cumplirse para la salida de la función dadas ciertas entradas. QuickCheck luego genera pruebas que intentan refutar estas suposiciones usando la implementación y, si encuentra una, la reduce a una entrada mínima que las demuestra falsas.

Curiosamente, la cantidad de escenarios que una prueba unitaria normalmente tiene que cubrir está fuertemente influenciada por el lenguaje de programación en el que está escrita. Lo cual me lleva necesariamente a discutir la sagrada guerra de opiniones sobre el tipado estático y dinámico.

Sistemas de tipos

Hindley-Milner

Los sistemas de tipos estáticos al estilo Hindley-Milner, como Haskell o Rust, obligan al programador a establecer contratos que se verifican antes de que el programa pueda ejecutarse.

Lo que el programador descubre, entonces, es que de repente está programando 2 lenguajes en paralelo: el sistema de tipos proporciona una prueba para el programa, mientras que también existe el programa que cumple con los requisitos.

Esto permite un estilo de razonamiento sobre la corrección que, combinado con un compilador útil, permitirá al programador centrarse en las cosas que no pueden ser demostradas por el sistema de tipos: la lógica de negocio.

Por supuesto, esto no es una victoria neta. En muchos casos, una solución que usa un sistema de tipos dinámico es mucho más sencilla y elegante, o, en otros casos, las restricciones del sistema de tipos hacen que ciertas implementaciones sean básicamente imposibles.

En otros casos, usar Haskell permitió escribir un programa mucho más pequeño de forma significativamente más rápida en comparación con las alternativas, tanto que la Marina de los EE. UU. tuvo que repetir la prueba con incredulidad.

La elegancia está en el ojo de quien mira, y una abstracción tipada de forma hermosa que se reduce a una simple máquina de estados durante la compilación puede ser tan atractiva como una rápida y sucia macro de LISP. A veces, toda esa complejidad es difícil de justificar solo para complacer al compilador. Viene con la experiencia, el gusto, y la aplicación de un juicio razonado en la situación correcta. La gente a menudo ve la programación como un oficio artesanal. Sí, sabemos cómo hacer los cálculos, pero con este ingenioso truco llegamos al 90% con el 10% del esfuerzo, y puede que sea suficientemente bueno. Y explotará en ese caso límite que pensé que nunca ocurriría realmente. Pero me estoy desviando del tema.

Débil pero estático

Algo más accesibles, pero que proporcionan una verificación menos estricta, son los lenguajes de la familia OOP al estilo C++/Java/C#, así como los del estilo C y Go.

Los sistemas de tipos aquí permiten un tipo diferente de flexibilidad, y más vías de escape deseables hacia el mundo dinámico.

Un sistema de tipos más débil, pero aún estático, proporciona menos garantías sobre la corrección de los programas, algo que tenemos que compensar en las pruebas y/o en los estándares de codificación. El Laboratorio de Propulsión a Chorro de la NASA, un fabricante en masa de rovers marcianos, mantiene un conjunto de pautas de programación segura para C. Sus pautas parecen ser efectivas. Opportunity superó sus 90 días de actividad originalmente planeados por 14 años gracias a un mantenimiento cuidadoso. Curiosity sigue recorriendo la superficie de Marte, y se le aplican parches de forma regular.

Dinámico

Hablando del JPL, el folclore de internet conserva la historia del uso de LISP en el laboratorio de la NASA, un lenguaje de programación funcional y dinámico de los años 60, que todavía se considera uno de los inventos, o incluso descubrimientos, más influyentes en la ciencia de la computación. El dialecto de LISP más usado hoy en día es Clojure, que está ganando popularidad en los círculos de ciencia de datos.

Los lenguajes dinámicos ofrecen una libertad máxima, y poca seguridad. Lo más común es que la única forma de determinar si una pieza de código es de alguna manera razonable es ejecutarla, lo que significa que nuestra estrategia de pruebas debe ser más sistemática y, de hecho, exhaustiva, ya que no hay una capa siguiente a la que recurrir.

Probablemente el uso más extendido de un sistema de tipos dinámico hoy en día proviene de JavaScript. Curiosamente, a medida que las empresas se esfuerzan por reducir la barrera de entrada para los colaboradores mientras escalan el desarrollo y mantienen la calidad, Google, Microsoft y Facebook idearon cada uno sus propias soluciones para introducir alguna forma de verificación estática de tipos en el lenguaje.

Aunque Dart de Google no ha visto una adopción significativa, TypeScript de Microsoft sí, y su uso está impulsado por proyectos grandes y populares como VSCode. Ambos enfoques introducen un lenguaje con verificación estática de tipos que compila a JavaScript, lo que facilita su introducción gradual también en proyectos existentes.

En contraste, Flow de Facebook es un analizador estático construido puramente sobre JavaScript, que introduce su propio tipo de anotaciones de tipo. La idea es que si hay anotaciones de tipo en lugares estratégicos, el verificador de tipos debería poder determinar si hay errores de tipo en alguna parte del programa rastreando el flujo de datos.

Los programadores entusiastas te dirán que ambos enfoques del tipado estático en JavaScript tienen sus propias fallas, y tendrían razón. Al final, muchos de los argumentos se reducen a ideas subjetivas y gustos sobre la arquitectura del software. Sin embargo, parece difícil negar que alguna forma de verificación estática de tipos proporciona varios beneficios para escalar y mantener proyectos de software.

Las pequeñas cosas que se nos escapan

La lista de cosas que podemos hacer para garantizar la corrección del software está lejos de terminar. El estado del arte sigue avanzando, y nuevos enfoques ganan popularidad rápidamente, especialmente dentro de la comunidad de seguridad.

En módulos absolutamente críticos, como todo lo relacionado con criptografía o seguridad, la verificación formal puede aumentar la confianza en partes del sistema, pero es difícil de escalar.

Se puede ver un sentimiento familiar detrás de los principios de LangSec. En muchos casos, el poder y la expresividad de nuestros lenguajes permiten que se cuelen errores inadvertidos. LangSec dice: haz que todos los estados inválidos sean irrepresentables por el propio lenguaje. Haz que el lenguaje limite lo que el programador puede hacer, para que pueda evitar lo que no debería hacer. Esta es también la motivación detrás de estándares de codificación como el del JPL, que permite razonar sobre el estado y el flujo de datos a lo largo del código del programa con mayor facilidad.

Cuando estamos razonablemente seguros de que lo que necesitamos es suficientemente bueno, podemos empezar a hacerle fuzzing. El fuzzing es genial. Se trata de alimentar estados inesperados a un sistema, y esperar a que causen fallos. Esta idea simple ayuda a descubrir agujeros de seguridad en software popular o puede ayudar a diseñar caos en la nube.

Como siempre, producir un sistema estable y seguro requiere una ingeniería basada en principios, tanto en software como en arquitectura. Necesitamos entender las piezas que componen el todo, analizar y luego verificar sus interacciones internamente, y con el entorno. A pesar de nuestros mejores esfuerzos, siempre se colarán errores, y lo único que podemos hacer es intentar asegurarnos de que los que queden no sean catastróficos.

Sin embargo, una vez que el software se pone en producción, la verificación no se detiene. Diseñar para la observabilidad exponiendo controles, trazado, alertas y recopilando un conjunto de métricas operativas nos ayuda a razonar sobre el estado del sistema mientras está funcionando, lo cual es la prueba definitiva de todo.

El desarrollo de software es un proceso, y es prácticamente imposible alcanzar la perfección. Mientras el equipo tenga un plan para aproximarse a ella, y todos estén comprometidos, podemos considerarlo suficientemente bueno, y luego salir de la oficina a disfrutar del sol.

Si te interesa seguir debatiendo sobre lo correcto e incorrecto de las pruebas de software, escríbeme – @rhapsodhy o por correo electrónico – ¡me encantaría conocer tu opinión!

Aquí en Red Sift, cuando no estamos debatiendo sobre la forma correcta de probar, estamos ayudando a organizaciones que priorizan la seguridad a comunicarse con éxito y garantizar la confianza de sus empleados, proveedores y clientes. Visita nuestra página de inicio para descubrir más sobre lo que hacemos.

Red Sift find out moreRed Sift find out more
Red Sift
Red Sift