IT & Engineering

Errores de software y cómo solucionarlos más rápido

El verdadero coste del software no reside en la creación de nuevo software, sino en su mantenimiento. Si esto es cierto, las estrategias que empleamos para ayudar a reducir los costes generales radican en el mantenimiento diario del código. ¿No nos crees? Sigue leyendo.
Imagen para Errores de software y cómo solucionarlos más rápido

El coste de la depuración no es el mismo para todo el mundo. El coste no depende solo de las tarifas de operación y servicio, sino de la cantidad de deuda técnica que tengas.

Al hablar de deuda técnica, nos referimos al coste en el que incurren las empresas al no solucionar problemas que les afectarán en el futuro. Esto puede salir caro, pero ¿y si existieran patrones o estrategias que el equipo de desarrollo pudiera aprovechar para reducir el tiempo que se tarda en identificar y resolver los errores?

En este artículo, predicamos con el ejemplo y compartimos una de las estrategias que consideramos más eficaces para reducir el tiempo de resolución de los errores.

¿Qué es un error de software?

Un error de software es simplemente un fallo de programación en un programa. Los errores son bastante comunes en un único programa o aplicación, pero, si ampliamos la perspectiva para observar toda una plataforma de servicios integrados y código interconectado, la idea de buscar errores empieza a parecerse a ese viejo dicho sobre buscar una aguja en un pajar.

Solucionar un error lleva más tiempo que escribir una línea de código. Por tanto, tiene sentido invertir en el desarrollo de tu software desde el principio. ¿Verdad?

El coste del éxito: procesos de desarrollo de software

Prepararse para el éxito conlleva un alto coste inicial, tanto de tiempo como de recursos. El desarrollo de software a medida puede tener una etiqueta de precio medio de hasta 250 000 $ dependiendo de tu infraestructura y tus necesidades, por lo que resulta fundamental para los costes contar con un plan para tu estrategia de mantenimiento frente a incurrir en el coste de crear código nuevo constantemente.

Modelado de tu entorno de producción

Tienes las mismas posibilidades de reproducir un error en un entorno local o controlado que un ratón de sobrevivir en una granja de gatos… a menos que hayas modelado tu entorno de producción con la mayor precisión posible.

Esta solución puede adoptar muchas formas, como la práctica habitual de crear un entorno de ensayo o de desarrollo. Sin embargo, son caros y los entornos muy duraderos tienden a desviarse con el tiempo. En los 20 años de experiencia de Derrick como desarrollador, no hay nada que se acerque a la capacidad de reproducir un problema en un equipo local. Esto significa que debes poder modelar el entorno de producción a nivel local de la forma más realista y reproducible posible.

Las implementaciones más exitosas de esto que hemos visto se encuentran en arquitecturas que utilizan microservicios o, como nos gusta llamarlos, “servicios de ámbito de dominio”. En estos entornos, el objetivo es levantar cada servicio dependiente para que el servicio en el que estás trabajando no pueda notar la diferencia entre ejecutarse en tu equipo local y ejecutarse en producción.

¿Cómo puedes conseguirlo? La forma más sencilla es exigir a las personas responsables del servicio que proporcionen una implementación simulada o ficticia fácil de usar de la interfaz pública de su servicio. Este es un ejemplo perfecto de cómo prepararse para el éxito. Crear estas simulaciones requiere tiempo y recursos. Si existe una cultura en el equipo de desarrollo orientada a proporcionar estas herramientas, el objetivo de modelar tu entorno de producción en el código resulta mucho más fácil, ya que la mayor parte del trabajo duro de implementar un servicio falso o simulado está hecho. Si no es así, tendrás que pagar el precio de crearlo por tu cuenta.

Si tu organización utiliza Golang, puedes importar servicios simulados directamente desde el código del servicio dependiente, lo que simplifica el hecho de mantenerse al día de los cambios.

Al simular tus servicios, no basta con recrear interfaces externas, sino que también tienes que incluir los protocolos de transporte. Puede que te estés preguntando, ¿por qué? ¿No sería mejor evitar realizar costosas llamadas remotas por completo? ¿Sería más barato? Sí, pero no sería eficaz. El objetivo es modelar el entorno de producción con la mayor precisión posible; esto incluye el transporte que usa tu aplicación para comunicarse con otros servicios. Ya sea HTTP, GRPC o un plan antiguo de TCP, las pequeñas variaciones en la pila de transporte o en las bibliotecas de serialización pueden tener repercusiones muy sutiles en cómo funciona tu código en producción.

Anécdota: cómo aprendimos esto en Mailgun

Teníamos un servicio que cambió las bibliotecas de serialización JSON con la intención de aumentar el rendimiento, pero esto introdujo sin querer un problema de análisis de Unicode. Simular la interfaz en lugar de llamar realmente al transporte y a la función de serialización nos habría ocultado este problema.

Otro problema inesperado implicó un cambio en la biblioteca DNS de Golang, que habría roto por completo nuestro entorno de producción de no ser por nuestro paquete de pruebas funcionales (y sí, ejecutamos una implementación simulada de DNS para nuestras pruebas).

Creación de pruebas funcionales para encontrar casos extremos y diagnosticar problemas

Bien, una vez que hemos modelado nuestro entorno, lo primero que debes hacer es crear un paquete de pruebas funcionales. Un paquete de pruebas funcionales es un contenedor que alberga un conjunto de pruebas diseñadas para ayudar a ejecutar y notificar los estados de ejecución de las pruebas. Puedes añadir casos de prueba y planes a tus paquetes para cubrir una variedad de situaciones y casos extremos. Cuanto mejor sea tu paquete, más fácil te resultará intentar diagnosticar un problema.

Si el problema que intentas solucionar existe como prueba funcional, puedes reemplazar fácil y rápidamente los valores de la prueba por los valores exactos que se encuentran en producción.  El simple hecho de reproducir el problema a nivel local recreando los datos exactos de producción puede conducir a la solución. Si has facilitado la tarea de importar datos de producción o de simular datos de producción, esta puede ser una herramienta muy valiosa a tu disposición.

Si no tienes ninguna prueba funcional que cubra esa situación (lo que es habitual, ya que los usuarios a menudo encuentran formas molestas… esto… únicas e imprevistas de usar tu sistema), tendrás que crearla.

Screenshot_2023-04-20_at_18
Autorretrato de profesional de ingeniería con frustración por no haber creado pruebas funcionales.

Parte de prepararnos para el éxito implica realizar pruebas funcionales o tener la capacidad de crear rápidamente una prueba funcional. Las pruebas unitarias (que prueban la porción más pequeña de código que puede aislarse de forma lógica en un sistema) pueden resultar útiles una vez que hayas acotado el problema. Una prueba funcional te permite trazar líneas generales sobre tu servicio y comprender desde la perspectiva del cliente cómo funciona dicho servicio en una situación concreta. Al depurar, probar el producto siempre es más importante que probar el código.

Crea pruebas funcionales automatizadas dentro de tus paquetes que ejecuten los casos de prueba de forma automática. Usa esto como parte de tu programa de control de calidad para descubrir y solucionar errores antes de que tu aplicación pase a producción.

Por qué deberías evitar las pruebas manuales y qué hacer en su lugar

Lo opuesto a las pruebas funcionales son las pruebas manuales. Las pruebas manuales son un tipo de pruebas de software en las que los casos de prueba son ejecutados manualmente por un profesional de pruebas sin utilizar ninguna herramienta automatizada. Siempre debes centrarte en las pruebas funcionales en lugar de en las manuales, ya que una prueba manual es propensa a errores y no es reproducible.

Gran parte del equipo de programación comete el error de ejecutar un servicio localmente y realizar pruebas manuales en los puntos de conexión para diagnosticar problemas. Después de haber hecho esto durante muchos años, no te podemos contar de primera mano la cantidad de veces que hemos reproducido un problema sin saber exactamente cómo lo logramos, lo que ha provocado que quisiéramos lanzar mesas por los aires al intentar desandar nuestros pasos.

Las sesiones de depuración a menudo pasan de durar horas a durar días, y con las pruebas manuales todos los escenarios que probamos quedan sin documentar y se difuminan entre sí. Sin embargo, si todos estos escenarios intentados se redactan como pruebas funcionales, cada escenario puede pasar a formar parte del historial de la sesión de depuración.

Si escribes pruebas funcionales para escenarios nuevos, pueden convertirse en parte del paquete de pruebas de regresión que se ejecutan durante la integración continua (CI).

Hay un secreto para las pruebas funcionales. Escribir pruebas funcionales para diagnosticar problemas es una estrategia clave para minimizar el tiempo de resolución de los errores. Sin embargo, este método de diagnóstico no funciona si escribir la prueba funcional resulta difícil. ¿La solución? Simplifica las cosas desde el primer paso.

Crea un paquete de funciones auxiliares y aserciones sencillas. El objetivo es que las acciones, como los bucles de reintento y la importación de paquetes de datos, resulten MUY fáciles de realizar. Estas funciones auxiliares deberían estar entre las primeras herramientas creadas durante la redacción inicial del software para brindar asistencia a las nuevas pruebas funcionales en el futuro.

Un ejemplo: cómo hicimos esto en Mailgun

La API principal de /messages en Mailgun está gestionada por un servicio al que llamamos influx. Este servicio incorpora tanto mensajes HTTP como SMTP en nuestro sistema y se comunica con prácticamente todos los demás servicios de nuestro paquete de servicios de envío de mensajes.

Como resultado, hemos invertido mucho en nuestro paquete de pruebas funcionales y hemos modelado nuestro entorno de producción en el código para esta parte tan pública de nuestro catálogo de servicios. Hemos creado más de 700 pruebas, la mayoría de las cuales son funcionales y se completan en unos seis minutos en un equipo local.

Muchas de estas pruebas se añadieron durante las sesiones de depuración o diagnóstico y luego se incorporaron a nuestro paquete de pruebas. Si se necesita un nuevo escenario, se añade al paquete funcional y sirve de protección frente a futuras regresiones.

Reflexiones finales

El verdadero coste del desarrollo de software reside en el mantenimiento y diagnóstico del código. Por eso, prepararse para el éxito mediante la creación de un paquete de herramientas que puedas usar para disminuir el tiempo de resolución justifica el tiempo y el dinero adicionales.

Esa inversión inicial seguirá dando sus frutos a medida que avance el proyecto.

¿Te ha gustado nuestra visión sobre cómo encontrar y diagnosticar errores de software? Si quieres más contenido como este, no olvides suscribirte a nuestra newsletter.

¡Mantenme informado/a! Recibe excelentes recursos en tu bandeja de entrada cada semana.
Envíame la newsletter de Mailjet. Acepto expresamente recibir la newsletter y sé que puedo darme de baja fácilmente en cualquier momento.

¡Consulta mensualmente tu bandeja de entrada para recibir la newsletter de Mailjet!